mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-06 03:38:24 +00:00
I18n improvements (#63)
Co-authored-by: minibits-cash <support@minibits.cash> Co-authored-by: deerwhisper2310 <142937144+deerwhisper2310@users.noreply.github.com>
This commit is contained in:
co-authored by
minibits-cash
deerwhisper2310
parent
9a38bbb826
commit
7c97e90324
@@ -137,7 +137,7 @@ Minibits wallet is in early beta and available as of now only for Android device
|
||||
|
||||
Minibits is a bare React Native app written in Typescript. The project structure and code itself are intentionally verbose to support readability. Critical wallet code is reasonably documented. However, there is vast space for existing code improvements, refactoring, and bug fixing. This is an early beta software and the author does not code for a living.
|
||||
|
||||
The code is derived from Ignite template, however with many libraries, notably Expo, stripped down to achieve fast startup times. Performance bottleneck on some Android devices is react-native-keychain. To overcome this, it has been patched not to warm-up on startup, caching for wallet operatoions is in place and its use to encrypt storage is opt-in.
|
||||
The code is derived from Ignite template, however with many libraries, notably Expo, stripped down to achieve fast startup times. Performance bottleneck on some Android devices is react-native-keychain. To overcome this, it has been patched not to warm-up on startup, caching for wallet operations is in place and its use to encrypt storage is opt-in.
|
||||
|
||||
Wallet state is managed by mobx-state-tree and persisted in fast MMKV storage. Only the basic mobx concepts are in place, whole model could be improved. All critical wallet code is in services/walletService.ts and all ecash state changes are in models/ProofsStore.ts. Wallet communication with the mints is in services/cashuMintClient.ts and uses [cashu-ts](https://github.com/cashubtc/cashu-ts) library.
|
||||
|
||||
|
||||
+108
-1
@@ -7,11 +7,47 @@ const en = {
|
||||
paste: "Paste",
|
||||
scan: "Scan",
|
||||
copy: "Copy",
|
||||
copyFailParam: "Couldn't copy %{param}",
|
||||
confirm: "Confirm",
|
||||
save: "Save",
|
||||
add: "Add",
|
||||
success: "Success!",
|
||||
offline: "Offline",
|
||||
qr: "QR Code",
|
||||
sats: "SATS",
|
||||
notCreated: 'Not yet created',
|
||||
resetDefault: 'Reset to default',
|
||||
share: "Share",
|
||||
},
|
||||
paymentCommon: {
|
||||
pay: "Pay",
|
||||
payMe: 'Pay me',
|
||||
paySatsAmountTo: "Pay %{amount} SATS to",
|
||||
payFromWallet: 'Pay from wallet',
|
||||
receiveInPerson: "Receive in person"
|
||||
},
|
||||
transactionCommon: {
|
||||
from: "From %{sender}",
|
||||
receivedFrom: "Received from %{sender}",
|
||||
sentTo: "Sent to %{receiver}",
|
||||
paidTo: "Paid to %{receiver}",
|
||||
youReceived: 'You received',
|
||||
youSent: 'You sent',
|
||||
youPaid: 'You paid',
|
||||
unknown: 'Unknown transaction',
|
||||
status: {
|
||||
completedFee: 'Completed %{fee}',
|
||||
draft: 'Draft',
|
||||
error: 'Error',
|
||||
pending: 'Pending',
|
||||
prepared: 'Prepared',
|
||||
reverted: 'Reverted',
|
||||
blocked: 'Blocked',
|
||||
expired: 'Expired'
|
||||
},
|
||||
tapToRedeem: 'Tap to redeem',
|
||||
redeemOnline: 'Redeem online'
|
||||
},
|
||||
welcomeScreen: {
|
||||
page1: {
|
||||
heading: "Welcome",
|
||||
@@ -48,6 +84,10 @@ const en = {
|
||||
topUpWalletSubText: "Top up your balance by paying Bitcoin lightning invoice from another wallet",
|
||||
transferFromWallet: "Transfer from wallet",
|
||||
transferFromWalletSubText: "Debit your balance by paying Bitcoin lightning invoice from this wallet",
|
||||
feeBadge: {
|
||||
final: "+ final fee %{fee} SATS",
|
||||
upto: "+ fee up to %{fee} SATS"
|
||||
}
|
||||
},
|
||||
receiveScreen: {
|
||||
title: "Receive",
|
||||
@@ -112,8 +152,75 @@ const en = {
|
||||
contactsScreen: {
|
||||
new: "New",
|
||||
scan: "Scan",
|
||||
newTitle: "Add new contact",
|
||||
newTitle: "Add new contact",
|
||||
getInvoice: "Get invoice",
|
||||
ownName: {
|
||||
donationSuccess: "Thank you! Donation for %{receiver} has been successfully paid.",
|
||||
tooShort: 'Write your wallet profile name to the text box, use min 2 characters.',
|
||||
illegalChar: 'Do not use . or - characters at the beginning or the end of name.',
|
||||
profileExists: 'This wallet name is already in use, choose another one.',
|
||||
chooseOwnName: 'Choose your own name',
|
||||
chooseOwnNameFooter: 'Use lowercase letters, numbers and .-_',
|
||||
available: "%{name} is available!",
|
||||
payToGetOwnName: "Pay the following lightning invoice and get your %{name} wallet name.",
|
||||
ctaPay: "Pay from wallet",
|
||||
insufficient: 'Your wallet balance is not enough to pay this invoice amount but you can still pay it from another wallet.',
|
||||
betaWarning: 'Please accept this is an early beta software. Your data can still be lost due to a bug or unexpected data loss.'
|
||||
},
|
||||
privateContacts: {
|
||||
selectSendPaymentRequest: 'Select contact to send your payment request to.',
|
||||
selectSendToken: 'Select contact to send your ecash to.',
|
||||
selectSendLnURL: 'Select contact to send Lightning payment to.',
|
||||
saveNewFormat: "Please enter a wallet address in name@domain.xyz format",
|
||||
profileNotFound: "Profile name %{name} could not be found. Make sure the name is correct.",
|
||||
noLightningAddress: 'This contact does not have a Lightning address, send ecash instead.',
|
||||
explainerText: "Private contacts",
|
||||
explainerSubText: "Add other Minibits users as your private contacts. Every user gets sharable @minibits.cash wallet address. You can pay privately to your contacts anytime even if they are offline.",
|
||||
switchName: "Switch your wallet name and picture?",
|
||||
switchNameSubText: 'Get cooler wallet name or profile picture. Select from an array of random names and images or opt for your own @minibits.cash wallet name.',
|
||||
bottomModal: "Private contacts are unique identifiers of other Minibits wallets. You can use them to send or request ecash and you can safely share your own with others.",
|
||||
domainMinibits: "Use minibits.cash domain",
|
||||
domainExternal: "Use another domain"
|
||||
},
|
||||
publicContacts: {
|
||||
npubPasteError: 'Copy your NPUB key first, then paste',
|
||||
relayurlPasteError: 'Copy your relay URL key first, then paste',
|
||||
relayExists: 'Relay already exists.',
|
||||
missingLightningAddress: 'This contact does not have a Lightning address, send ecash instead.',
|
||||
nostrTip: 'Tip the people you follow',
|
||||
nostrTipSubText: 'Add your NOSTR social network public key (npub) and tip or donate to your favourite people and projects directly from the minibits wallet.',
|
||||
nostrSetPublicKey: 'Set your public key',
|
||||
nostrSetPublicKeySubText: 'Add or change your NOSTR social network public key (npub).',
|
||||
nostrSetRelay: 'Set relay',
|
||||
nostrSetRelaySubText: 'Add or change your own relay if your profile and follows are not hosted on the default relays.',
|
||||
nostrRemovePub: 'Remove your public key',
|
||||
nostrRemovePubSubText: 'Remove your npub key and stop loading public contacts.',
|
||||
addNpub: 'Add your npub key',
|
||||
pasteDemoKey: 'Paste demo key',
|
||||
setOwnRelay: 'Set your own relay'
|
||||
},
|
||||
randomName: {
|
||||
selectOneOfUsernames: 'Select one of the usernames'
|
||||
}
|
||||
},
|
||||
paymentRequestScreen: {
|
||||
incoming: {
|
||||
noRequests: 'There are no incoming payment requests to be paid or they have already expired.'
|
||||
},
|
||||
outgoing: {
|
||||
noRequests: 'There are no outgoing payment requests to be paid or they have already expired.',
|
||||
invoiceShared: 'Lightning invoice has been shared, waiting to be paid by receiver.',
|
||||
sharingCancelled: 'Sharing cancelled',
|
||||
scanAndPay: 'Scan and pay to top-up'
|
||||
},
|
||||
// might move this to common later if it gets used in other screens
|
||||
listItem: {
|
||||
expires: "Expires %{datetime}",
|
||||
expired: "Expired %{datetime}",
|
||||
requestPaymentSuccess: 'Request has been paid'
|
||||
}
|
||||
},
|
||||
|
||||
profileScreen: {
|
||||
changeAvatar: "Change picture",
|
||||
changeAvatarSubtext: "Select one from randomly generated pictures.",
|
||||
|
||||
+21
-14
@@ -1,7 +1,6 @@
|
||||
|
||||
import { I18nOptions } from "i18n-js"
|
||||
import { i18n } from "./i18n"
|
||||
import { TxKeyPath } from "./i18n"
|
||||
import {I18nOptions} from 'i18n-js'
|
||||
import {i18n} from './i18n'
|
||||
import {TxKeyPath} from './i18n'
|
||||
|
||||
/**
|
||||
* Translates text.
|
||||
@@ -12,21 +11,29 @@ import { TxKeyPath } from "./i18n"
|
||||
*
|
||||
* @example
|
||||
* Translations:
|
||||
*
|
||||
* ```en.ts
|
||||
* ```ts
|
||||
* // en.ts
|
||||
* {
|
||||
* "hello": "Hello, {{name}}!"
|
||||
* "heading": "Welcome to the app!",
|
||||
* "buttonText": "Donate",
|
||||
* "greeting": "Hello, %{name}!" // parameter
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Usage:
|
||||
* ```ts
|
||||
* import { translate } from "i18n-js"
|
||||
* ```tsx
|
||||
* import { translate } from "../../i18n";
|
||||
* // or whatever the path is, let it auto-import
|
||||
*
|
||||
* translate("common.ok", { name: "world" })
|
||||
* // => "Hello world!"
|
||||
* translate("heading") // => Welcome to the app!
|
||||
* translate("greeting", { name: "world" }) // => Hello world!
|
||||
* <Button tx="buttonText" /> // => a Donate button
|
||||
* ```
|
||||
* You can use nested objects, access them like: `common.payment.success`
|
||||
*/
|
||||
export function translate(key: TxKeyPath, options?: I18nOptions) {
|
||||
return i18n.t(key, options)
|
||||
|
||||
export function translate(
|
||||
key: TxKeyPath,
|
||||
options?: Partial<I18nOptions> & {[parameter: string]: any},
|
||||
) {
|
||||
return i18n.t(key, options as I18nOptions)
|
||||
}
|
||||
|
||||
@@ -37,8 +37,7 @@ export const BackupScreen: FC<SettingsStackScreenProps<'Backup'>> = observer(fun
|
||||
)
|
||||
const [error, setError] = useState<AppError | undefined>()
|
||||
const [info, setInfo] = useState('')
|
||||
const [isBackupModalVisible, setIsBackupModalVisible] =
|
||||
useState<boolean>(false)
|
||||
const [isBackupModalVisible, setIsBackupModalVisible] = useState<boolean>(false)
|
||||
const [isHandleSpentFromSpendavleSentToQueue, setIsHandleSpentFromSpendavleSentToQueue] = useState<boolean>(false)
|
||||
const [backupResultMessage, setBackupResultMessage] = useState<string>()
|
||||
const [totalSpentCount, setTotalSpentCount] = useState<number>(0)
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ResultModalInfo } from '../Wallet/ResultModalInfo'
|
||||
import { MINIBITS_NIP05_DOMAIN } from '@env'
|
||||
import { useFocusEffect } from '@react-navigation/native'
|
||||
import { SendOption } from '../SendOptionsScreen'
|
||||
import { translate } from '../../i18n'
|
||||
|
||||
const DEFAULT_DONATION_AMOUNT = 100
|
||||
|
||||
@@ -101,7 +102,7 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
|
||||
stopPolling(`checkDonationPaidPoller-${donationInvoice.payment_hash}`)
|
||||
setResultModalInfo({
|
||||
status: TransactionStatus.COMPLETED,
|
||||
message: `Thank you! Donation for ${ownName+MINIBITS_NIP05_DOMAIN} has been successfully paid.`
|
||||
message: translate("contactsScreen.ownName.donationSuccess", { receiver: ownName+MINIBITS_NIP05_DOMAIN })
|
||||
})
|
||||
toggleResultModal()
|
||||
resetState()
|
||||
@@ -150,12 +151,12 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
|
||||
|
||||
const onOwnNameCheck = async function () {
|
||||
if(!ownName || ownName.length < 2) {
|
||||
setInfo('Write your wallet profile name to the text box, use min 2 characters.')
|
||||
setInfo(translate('contactsScreen.ownName.tooShort'))
|
||||
return
|
||||
}
|
||||
|
||||
if(!isValidName(ownName)) {
|
||||
setInfo('Do not use . or - characters at the beginning or the end of name.')
|
||||
setInfo(translate('contactsScreen.ownName.illegalChar'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -163,7 +164,7 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
|
||||
const profileExists = await MinibitsClient.getWalletProfileByNip05(ownName + MINIBITS_NIP05_DOMAIN)
|
||||
|
||||
if(profileExists) {
|
||||
setInfo('This wallet name is already in use, choose another one.')
|
||||
setInfo(translate("contactsScreen.ownName.profileExists"))
|
||||
return
|
||||
}
|
||||
setIsChecked(true)
|
||||
@@ -265,7 +266,7 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
|
||||
<View style={$contentContainer}>
|
||||
<Card
|
||||
style={[$card, {marginTop: spacing.small}]}
|
||||
heading='Choose your own name'
|
||||
headingTx='contactsScreen.ownName.chooseOwnName'
|
||||
headingStyle={{textAlign: 'center'}}
|
||||
ContentComponent={
|
||||
<View style={$ownNameContainer}>
|
||||
@@ -292,7 +293,7 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
footer={'Use lowercase letters, numbers and .-_' }
|
||||
footerTx='contactsScreen.ownName.chooseOwnNameFooter'
|
||||
footerStyle={{color: hint, textAlign: 'center'}}
|
||||
/>
|
||||
</View>
|
||||
@@ -305,13 +306,13 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
|
||||
</View>
|
||||
<View style={{alignItems: 'center'}}>
|
||||
<Text
|
||||
text={`${ownName} is available!`}
|
||||
text={translate('contactsScreen.ownName.available',{ name: ownName })}
|
||||
style={{fontSize: 18}}
|
||||
/>
|
||||
{donationInvoice ? (
|
||||
<>
|
||||
<Text
|
||||
text={`Pay the following lightning invoice and get your ${ownName+MINIBITS_NIP05_DOMAIN} wallet name.`}
|
||||
text={translate("contactsScreen.ownName.payToGetOwnName", { name: ownName+MINIBITS_NIP05_DOMAIN })}
|
||||
style={[$supportText, {color: hint}]}
|
||||
/>
|
||||
{isQRcodeVisible ? (
|
||||
@@ -341,12 +342,12 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
|
||||
<Button
|
||||
preset="default"
|
||||
style={{marginRight: spacing.small}}
|
||||
text="Pay from wallet"
|
||||
tx='contactsScreen.ownName.ctaPay'
|
||||
onPress={onPayDonation}
|
||||
/>
|
||||
<Button
|
||||
preset="secondary"
|
||||
text="Cancel"
|
||||
tx='common.cancel'
|
||||
onPress={resetState}
|
||||
/>
|
||||
</View>
|
||||
@@ -355,14 +356,14 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
|
||||
<Text
|
||||
size='xs'
|
||||
style={{textAlign: 'center', margin: spacing.medium}}
|
||||
text='Your wallet balance is not enough to pay this invoice amount but you can still pay it from another wallet.'
|
||||
tx="contactsScreen.ownName.insufficient"
|
||||
/>
|
||||
<View style={$payButtonContainer}>
|
||||
{isQRcodeVisible ? (
|
||||
<Button
|
||||
preset="default"
|
||||
style={{marginRight: spacing.small, maxHeight: 50}}
|
||||
text="Back"
|
||||
tx='common.back'
|
||||
onPress={() => setIsQRCodeVisible(false)}
|
||||
/>
|
||||
|
||||
@@ -377,13 +378,13 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
|
||||
size={spacing.small}
|
||||
/>
|
||||
)}
|
||||
text="QR code"
|
||||
tx='common.qr'
|
||||
onPress={() => setIsQRCodeVisible(true)}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
preset="secondary"
|
||||
text="Cancel"
|
||||
tx='common.cancel'
|
||||
onPress={resetState}
|
||||
/>
|
||||
</View>
|
||||
@@ -405,7 +406,7 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
|
||||
preset='heading'
|
||||
/>
|
||||
<Text
|
||||
text={`SATS`}
|
||||
tx='common.sats'
|
||||
style={{fontSize: 18, marginLeft: spacing.extraSmall}}
|
||||
/>
|
||||
</View>
|
||||
@@ -444,18 +445,18 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
|
||||
<Button
|
||||
preset="default"
|
||||
style={{marginRight: spacing.small}}
|
||||
text="Get invoice"
|
||||
tx='contactsScreen.getInvoice'
|
||||
onPress={onCreateDonation}
|
||||
/>
|
||||
<Button
|
||||
preset="secondary"
|
||||
text="Cancel"
|
||||
tx='common.cancel'
|
||||
onPress={resetState}
|
||||
/>
|
||||
</View>
|
||||
<View style={{flexDirection: 'row', alignItems: 'center', margin: spacing.medium}}>
|
||||
<Icon icon='faInfoCircle' />
|
||||
<Text style={{color: hint}} size='xxs' text='Please accept this is an early beta software. Your data can still be lost due to a bug or unexpected data loss.'/>
|
||||
<Text style={{color: hint}} size='xxs' tx='contactsScreen.ownName.betaWarning'/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
@@ -474,7 +475,7 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
|
||||
<ResultModalInfo
|
||||
icon="faCheckCircle"
|
||||
iconColor={colors.palette.success200}
|
||||
title="Success!"
|
||||
title={translate("common.success")}
|
||||
message={resultModalInfo?.message as string}
|
||||
/>
|
||||
<View style={$payButtonContainer}>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ReceiveOption } from '../ReceiveOptionsScreen'
|
||||
import { SendOption } from '../SendOptionsScreen'
|
||||
import { infoMessage, warningMessage } from '../../utils/utils'
|
||||
import { IncomingDataType, IncomingParser } from '../../services/incomingParser'
|
||||
import { translate } from '../../i18n'
|
||||
|
||||
|
||||
|
||||
@@ -39,15 +40,15 @@ export const PrivateContacts = observer(function (props: {
|
||||
const { paymentOption } = props
|
||||
|
||||
if (paymentOption && paymentOption === ReceiveOption.SEND_PAYMENT_REQUEST) {
|
||||
infoMessage('Select contact to send your payment request to.')
|
||||
infoMessage(translate("contactsScreen.privateContacts.selectSendPaymentRequest"))
|
||||
}
|
||||
|
||||
if (paymentOption && paymentOption === SendOption.SEND_TOKEN) {
|
||||
infoMessage('Select contact to send your ecash to.')
|
||||
infoMessage(translate("contactsScreen.privateContacts.selectSendToken"))
|
||||
}
|
||||
|
||||
if (paymentOption && paymentOption === SendOption.LNURL_ADDRESS) {
|
||||
infoMessage('Select contact to send Lightning payment to.')
|
||||
infoMessage(translate("contactsScreen.privateContacts.selectSendLnURL"))
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -80,14 +81,13 @@ export const PrivateContacts = observer(function (props: {
|
||||
log.trace('Start', newContactName, 'saveNewContact')
|
||||
|
||||
if(!newContactName) {
|
||||
setInfo(`Please enter a wallet address in name@domain.xyz format`)
|
||||
setInfo(translate("contactsScreen.privateContacts.saveNewFormat"))
|
||||
return
|
||||
}
|
||||
|
||||
toggleNewContactModal() // close
|
||||
setIsLoading(true)
|
||||
|
||||
|
||||
try {
|
||||
|
||||
let newContact: Contact | undefined = undefined
|
||||
@@ -117,7 +117,7 @@ export const PrivateContacts = observer(function (props: {
|
||||
const profileRecord = await MinibitsClient.getWalletProfileByNip05(newContactName + MINIBITS_NIP05_DOMAIN)
|
||||
|
||||
if(!profileRecord) {
|
||||
warningMessage(`Profile name ${newContactName + MINIBITS_NIP05_DOMAIN} could not be found. Make sure the name is correct.`)
|
||||
warningMessage(translate("contactsScreen.privateContacts.profileNotFound", { name: newContactName + MINIBITS_NIP05_DOMAIN }))
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
@@ -214,7 +214,7 @@ export const PrivateContacts = observer(function (props: {
|
||||
|
||||
if(paymentOption && paymentOption === SendOption.LNURL_ADDRESS) {
|
||||
if(!contact.lud16) {
|
||||
setInfo('This contact does not have a Lightning address, send ecash instead.')
|
||||
setInfo(translate("contactsScreen.privateContacts.noLightningAddress"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -258,120 +258,141 @@ export const PrivateContacts = observer(function (props: {
|
||||
const inputBg = useThemeColor('background')
|
||||
|
||||
return (
|
||||
<Screen contentContainerStyle={$screen}>
|
||||
<Screen contentContainerStyle={$screen}>
|
||||
<View style={$contentContainer}>
|
||||
{contactsStore.count > 0 ? (
|
||||
{contactsStore.count > 0 ? (
|
||||
<Card
|
||||
ContentComponent={
|
||||
<>
|
||||
<FlatList<Contact>
|
||||
data={contactsStore.all as Contact[]}
|
||||
renderItem={({ item, index }) => {
|
||||
return(
|
||||
<ContactListItem
|
||||
contact={item}
|
||||
isFirst={index === 0}
|
||||
gotoContactDetail={() => gotoContactDetail(item)}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
keyExtractor={(item) => item.pubkey}
|
||||
style={{ flexGrow: 0 }}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
style={$card}
|
||||
ContentComponent={
|
||||
<FlatList<Contact>
|
||||
data={contactsStore.all as Contact[]}
|
||||
renderItem={({item, index}) => {
|
||||
return (
|
||||
<ContactListItem
|
||||
contact={item}
|
||||
isFirst={index === 0}
|
||||
gotoContactDetail={() => gotoContactDetail(item)}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
keyExtractor={item => item.pubkey}
|
||||
style={{flexGrow: 0}}
|
||||
/>
|
||||
}
|
||||
style={$card}
|
||||
/>
|
||||
) : (
|
||||
<Card
|
||||
ContentComponent={
|
||||
<>
|
||||
<ListItem
|
||||
leftIcon='faComment'
|
||||
leftIconInverse={true}
|
||||
leftIconColor={colors.palette.iconGreen200}
|
||||
text='Private contacts'
|
||||
subText={"Add other Minibits users as your private contacts. Every user gets sharable @minibits.cash wallet address. You can pay privately to your contacts anytime even if they are offline."}
|
||||
onPress={gotoNew}
|
||||
/>
|
||||
<ListItem
|
||||
leftIcon='faCircleUser'
|
||||
leftIconInverse={true}
|
||||
leftIconColor={colors.palette.iconMagenta200}
|
||||
text="Switch your wallet name and picture?"
|
||||
subText={"Get cooler wallet name or profile picture. Select from an array of random names and images or opt for your own @minibits.cash wallet name."}
|
||||
onPress={gotoProfile}
|
||||
topSeparator={true}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
style={$card}
|
||||
/>
|
||||
)}
|
||||
{isLoading && <Loading />}
|
||||
) : (
|
||||
<Card
|
||||
ContentComponent={
|
||||
<>
|
||||
<ListItem
|
||||
leftIcon="faComment"
|
||||
leftIconInverse={true}
|
||||
leftIconColor={colors.palette.iconGreen200}
|
||||
tx='contactsScreen.privateContacts.explainerText'
|
||||
subTx="contactsScreen.privateContacts.explainerSubText"
|
||||
onPress={gotoNew}
|
||||
/>
|
||||
<ListItem
|
||||
leftIcon="faCircleUser"
|
||||
leftIconInverse={true}
|
||||
leftIconColor={colors.palette.iconMagenta200}
|
||||
tx="contactsScreen.privateContacts.switchName"
|
||||
subTx='contactsScreen.privateContacts.switchNameSubText'
|
||||
onPress={gotoProfile}
|
||||
topSeparator={true}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
style={$card}
|
||||
/>
|
||||
)}
|
||||
{isLoading && <Loading />}
|
||||
</View>
|
||||
<View style={$bottomContainer}>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
tx={'contactsScreen.new'}
|
||||
LeftAccessory={() => (
|
||||
<Icon
|
||||
icon='faCircleUser'
|
||||
color='white'
|
||||
size={spacing.medium}
|
||||
/>
|
||||
)}
|
||||
onPress={gotoNew}
|
||||
style={$buttonNew}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
tx={'contactsScreen.new'}
|
||||
LeftAccessory={() => (
|
||||
<Icon icon="faCircleUser" color="white" size={spacing.medium} />
|
||||
)}
|
||||
onPress={gotoNew}
|
||||
style={$buttonNew}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<BottomModal
|
||||
isVisible={isNewContactModalVisible ? true : false}
|
||||
ContentComponent={
|
||||
<View style={$newContainer}>
|
||||
<Text tx='contactsScreen.newTitle' preset="subheading" />
|
||||
<Text size='xxs' style={{color: domainText}} text='Private contacts are unique identifiers of other Minibits wallets. You can use them to send or request ecash and you can safely share your own with others.' />
|
||||
<View style={{flexDirection: 'row', alignItems: 'center', marginTop: spacing.small}}>
|
||||
<TextInput
|
||||
ref={contactNameInputRef}
|
||||
onChangeText={newContactName => setNewContactName(newContactName)}
|
||||
value={newContactName}
|
||||
autoCapitalize='none'
|
||||
keyboardType='default'
|
||||
maxLength={60}
|
||||
selectTextOnFocus={true}
|
||||
style={[$contactInput, {backgroundColor: inputBg}, (isExternalDomain) && {marginRight: spacing.small, borderTopRightRadius: spacing.small, borderBottomRightRadius: spacing.small}]}
|
||||
/>
|
||||
{!isExternalDomain && (
|
||||
<View style={[$contactDomain, { backgroundColor: inputBg}]}>
|
||||
<Text size='xxs' style={{color: domainText}} text={MINIBITS_NIP05_DOMAIN}/>
|
||||
</View>
|
||||
)}
|
||||
<Button
|
||||
tx={'common.save'}
|
||||
style={{
|
||||
borderRadius: spacing.small,
|
||||
marginRight: spacing.small,
|
||||
}}
|
||||
onPress={saveNewContact}
|
||||
/>
|
||||
</View>
|
||||
<Button
|
||||
preset='tertiary'
|
||||
text={isExternalDomain ? 'Use minibits.cash domain' : 'Use another domain'}
|
||||
onPress={toggleExternalDomain}
|
||||
style={{alignSelf: 'flex-start', minHeight: verticalScale(30)}}
|
||||
textStyle={{lineHeight: verticalScale(16), fontSize: 12}}
|
||||
isVisible={isNewContactModalVisible ? true : false}
|
||||
ContentComponent={
|
||||
<View style={$newContainer}>
|
||||
<Text tx="contactsScreen.newTitle" preset="subheading" />
|
||||
<Text
|
||||
size="xxs"
|
||||
style={{color: domainText}}
|
||||
tx="contactsScreen.privateContacts.bottomModal"
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: spacing.small,
|
||||
}}>
|
||||
<TextInput
|
||||
ref={contactNameInputRef}
|
||||
onChangeText={newContactName =>
|
||||
setNewContactName(newContactName)
|
||||
}
|
||||
value={newContactName}
|
||||
autoCapitalize="none"
|
||||
keyboardType="default"
|
||||
maxLength={60}
|
||||
selectTextOnFocus={true}
|
||||
style={[
|
||||
$contactInput,
|
||||
{backgroundColor: inputBg},
|
||||
isExternalDomain && {
|
||||
marginRight: spacing.small,
|
||||
borderTopRightRadius: spacing.small,
|
||||
borderBottomRightRadius: spacing.small,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{!isExternalDomain && (
|
||||
<View style={[$contactDomain, {backgroundColor: inputBg}]}>
|
||||
<Text
|
||||
size="xxs"
|
||||
style={{color: domainText}}
|
||||
text={MINIBITS_NIP05_DOMAIN}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
onBackButtonPress={toggleNewContactModal}
|
||||
onBackdropPress={toggleNewContactModal}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<Button
|
||||
tx={'common.save'}
|
||||
style={{
|
||||
borderRadius: spacing.small,
|
||||
marginRight: spacing.small,
|
||||
}}
|
||||
onPress={saveNewContact}
|
||||
/>
|
||||
</View>
|
||||
<Button
|
||||
preset="tertiary"
|
||||
tx={
|
||||
isExternalDomain
|
||||
? 'contactsScreen.privateContacts.domainMinibits'
|
||||
: 'contactsScreen.privateContacts.domainExternal'
|
||||
}
|
||||
onPress={toggleExternalDomain}
|
||||
style={{alignSelf: 'flex-start', minHeight: verticalScale(30)}}
|
||||
textStyle={{lineHeight: verticalScale(16), fontSize: 12}}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
onBackButtonPress={toggleNewContactModal}
|
||||
onBackdropPress={toggleNewContactModal}
|
||||
/>
|
||||
{info && <InfoModal message={info} />}
|
||||
{error && <ErrorModal error={error} />}
|
||||
</Screen>
|
||||
</Screen>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Icon, Text } from "../../components"
|
||||
import { useStores } from "../../models"
|
||||
import { spacing, useThemeColor } from "../../theme"
|
||||
import { getImageSource } from '../../utils/utils'
|
||||
import { translate } from "../../i18n"
|
||||
|
||||
|
||||
export interface ProfileHeaderProps {
|
||||
@@ -36,7 +37,7 @@ export const ProfileHeader = observer(function (props: ProfileHeaderProps) {
|
||||
color={'white'}
|
||||
/>
|
||||
)}
|
||||
<Text preset='bold' text={nip05 || 'Not yet created'} style={{color: 'white', marginBottom: spacing.small}} />
|
||||
<Text preset='bold' text={nip05 || translate("common.notCreated")} style={{color: 'white', marginBottom: spacing.small}} />
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ReceiveOption } from '../ReceiveOptionsScreen'
|
||||
import { useSafeAreaInsetsStyle } from '../../utils/useSafeAreaInsetsStyle'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { IncomingDataType, IncomingParser } from '../../services/incomingParser'
|
||||
import { translate } from '../../i18n'
|
||||
|
||||
|
||||
// const defaultPublicNpub = 'npub14n7frsyufzqsxlvkx8vje22cjah3pcwnnyqncxkuj2243jvt9kmqsdgs52'
|
||||
@@ -149,8 +150,8 @@ export const PublicContacts = observer(function (props: {
|
||||
|
||||
if(event.kind === 3) {
|
||||
const pubkeys = event.tags
|
||||
.filter((item: [string, string]) => item[0] === "p")
|
||||
.map((item: [string, string]) => item[1])
|
||||
.filter((item) => item[0] === "p")
|
||||
.map((item) => item[1])
|
||||
|
||||
log.trace('Following pubkeys:', pubkeys.length)
|
||||
setFollowingPubkeys(pubkeys)
|
||||
@@ -213,7 +214,7 @@ export const PublicContacts = observer(function (props: {
|
||||
const onPastePublicPubkey = async function () {
|
||||
const key = await Clipboard.getString()
|
||||
if (!key) {
|
||||
setInfo('Copy your NPUB key first, then paste')
|
||||
setInfo(translate("contactsScreen.publicContacts.npubPasteError"))
|
||||
return
|
||||
}
|
||||
setNewPublicPubkey(key)
|
||||
@@ -263,7 +264,7 @@ export const PublicContacts = observer(function (props: {
|
||||
const onPastePublicRelay = async function () {
|
||||
const url = await Clipboard.getString()
|
||||
if (!url) {
|
||||
setInfo('Copy your relay URL key first, then paste')
|
||||
setInfo(translate("contactsScreen.publicContacts.relayurlPasteError"))
|
||||
return
|
||||
}
|
||||
setNewPublicRelay(url)
|
||||
@@ -274,7 +275,7 @@ export const PublicContacts = observer(function (props: {
|
||||
try {
|
||||
if(newPublicRelay) {
|
||||
if(relaysStore.alreadyExists(newPublicRelay)) {
|
||||
setInfo('Relay already exists.')
|
||||
setInfo(translate("contactsScreen.publicContacts.relayExists"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -385,7 +386,7 @@ export const PublicContacts = observer(function (props: {
|
||||
|
||||
if(paymentOption && paymentOption === SendOption.LNURL_ADDRESS) {
|
||||
if(!contact.lud16) {
|
||||
setInfo('This contact does not have a Lightning address, send ecash instead.')
|
||||
setInfo(translate('contactsScreen.publicContacts.missingLightningAddress'))
|
||||
//reset
|
||||
navigation.setParams({
|
||||
paymentOption: undefined,
|
||||
@@ -445,8 +446,8 @@ export const PublicContacts = observer(function (props: {
|
||||
leftIcon='faComment'
|
||||
leftIconInverse={true}
|
||||
leftIconColor={colors.palette.iconViolet200}
|
||||
text='Tip the people you follow'
|
||||
subText={'Add your NOSTR social network public key (npub) and tip or donate to your favourite people and projects directly from the minibits wallet.'}
|
||||
tx="contactsScreen.publicContacts.nostrTip"
|
||||
subTx='contactsScreen.publicContacts.nostrTipSubText'
|
||||
onPress={toggleNpubModal}
|
||||
/>
|
||||
}
|
||||
@@ -528,22 +529,22 @@ export const PublicContacts = observer(function (props: {
|
||||
<>
|
||||
<ListItem
|
||||
leftIcon='faKey'
|
||||
text='Set your public key'
|
||||
subText={'Add or change your NOSTR social network public key (npub).'}
|
||||
tx="contactsScreen.publicContacts.nostrSetPublicKey"
|
||||
subTx='contactsScreen.publicContacts.nostrSetPublicKeySubText'
|
||||
onPress={toggleNpubModal}
|
||||
bottomSeparator={true}
|
||||
/>
|
||||
<ListItem
|
||||
leftIcon='faCircleNodes'
|
||||
text='Set relay'
|
||||
subText={'Add or change your own relay if your profile and follows are not hosted on the default relays.'}
|
||||
tx='contactsScreen.publicContacts.nostrSetRelay'
|
||||
subTx='contactsScreen.publicContacts.nostrSetRelaySubText'
|
||||
onPress={toggleRelayModal}
|
||||
bottomSeparator={true}
|
||||
/>
|
||||
<ListItem
|
||||
leftIcon='faBan'
|
||||
text='Remove your public key'
|
||||
subText={'Remove your npub key and stop loading public contacts.'}
|
||||
tx="contactsScreen.publicContacts.nostrRemovePub"
|
||||
subTx="contactsScreen.publicContacts.nostrRemovePubSubText"
|
||||
onPress={onRemovePublicPubKey}
|
||||
/>
|
||||
</>
|
||||
@@ -555,7 +556,7 @@ export const PublicContacts = observer(function (props: {
|
||||
isVisible={isNpubModalVisible}
|
||||
ContentComponent={
|
||||
<View style={$newContainer}>
|
||||
<Text text='Add your npub key' preset="subheading" />
|
||||
<Text tx="contactsScreen.publicContacts.addNpub" preset="subheading" />
|
||||
<View style={{flexDirection: 'row', alignItems: 'center', marginTop: spacing.small}}>
|
||||
<TextInput
|
||||
ref={npubInputRef}
|
||||
@@ -581,8 +582,8 @@ export const PublicContacts = observer(function (props: {
|
||||
/>
|
||||
</View>
|
||||
<View style={[$buttonContainer, {marginTop: spacing.medium}]}>
|
||||
<Button preset='tertiary' onPress={() => setNewPublicPubkey(defaultPublicNpub)} text='Paste demo key'/>
|
||||
<Button preset='tertiary' onPress={toggleNpubModal} text='Cancel'/>
|
||||
<Button preset='tertiary' onPress={() => setNewPublicPubkey(defaultPublicNpub)} tx="contactsScreen.publicContacts.pasteDemoKey"/>
|
||||
<Button preset='tertiary' onPress={toggleNpubModal} tx="common.cancel"/>
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
@@ -593,7 +594,7 @@ export const PublicContacts = observer(function (props: {
|
||||
isVisible={isRelayModalVisible ? true : false}
|
||||
ContentComponent={
|
||||
<View style={$newContainer}>
|
||||
<Text text='Set your own relay' preset="subheading" />
|
||||
<Text tx="contactsScreen.publicContacts.setOwnRelay" preset="subheading" />
|
||||
<View style={{flexDirection: 'row', alignItems: 'center', marginTop: spacing.small}}>
|
||||
<TextInput
|
||||
ref={relayInputRef}
|
||||
@@ -620,9 +621,9 @@ export const PublicContacts = observer(function (props: {
|
||||
</View>
|
||||
<View style={[$buttonContainer, {marginTop: spacing.medium}]}>
|
||||
{newPublicRelay && (
|
||||
<Button preset='tertiary' onPress={onRemovePublicRelay} text='Reset to default'/>
|
||||
<Button preset='tertiary' onPress={onRemovePublicRelay} tx="common.resetDefault"/>
|
||||
)}
|
||||
<Button preset='tertiary' onPress={toggleRelayModal} text='Cancel'/>
|
||||
<Button preset='tertiary' onPress={toggleRelayModal} tx="common.cancel"/>
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
|
||||
+125
-114
@@ -1,140 +1,151 @@
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {TextStyle, View, ViewStyle, InteractionManager, FlatList } from 'react-native'
|
||||
import {
|
||||
TextStyle,
|
||||
View,
|
||||
ViewStyle,
|
||||
InteractionManager,
|
||||
FlatList,
|
||||
} from 'react-native'
|
||||
import {spacing} from '../../theme'
|
||||
import {Button, Card, ErrorModal, InfoModal, Loading, Screen, Text} from '../../components'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
ErrorModal,
|
||||
InfoModal,
|
||||
Loading,
|
||||
Screen,
|
||||
Text,
|
||||
} from '../../components'
|
||||
import {useStores} from '../../models'
|
||||
import AppError, { Err } from '../../utils/AppError'
|
||||
import AppError, {Err} from '../../utils/AppError'
|
||||
import {$sizeStyles} from '../../components/Text'
|
||||
import {getRandomUsername} from '../../utils/usernames'
|
||||
import { scale } from '@gocodingnow/rn-size-matters'
|
||||
import {scale} from '@gocodingnow/rn-size-matters'
|
||||
import {translate} from '../../i18n'
|
||||
|
||||
export const RandomName = observer(function (props: {
|
||||
navigation: any
|
||||
pubkey: string
|
||||
}) {
|
||||
// const navigation = useNavigation()
|
||||
const {userSettingsStore, walletProfileStore} = useStores()
|
||||
const {navigation} = props
|
||||
|
||||
export const RandomName = observer(function (props: {navigation: any, pubkey: string}) {
|
||||
// const navigation = useNavigation()
|
||||
const {userSettingsStore, walletProfileStore} = useStores()
|
||||
const {navigation} = props
|
||||
const [randomNames, setRandomNames] = useState<string[]>([])
|
||||
const [selectedName, setSelectedName] = useState<string>('')
|
||||
const [info, setInfo] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<AppError | undefined>()
|
||||
|
||||
const [randomNames, setRandomNames] = useState<string[]>([])
|
||||
const [selectedName, setSelectedName] = useState<string>('')
|
||||
const [info, setInfo] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<AppError | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
setIsLoading(true)
|
||||
InteractionManager.runAfterInteractions(async () => {
|
||||
let i = 0
|
||||
let names = []
|
||||
while(i < 8) {
|
||||
const name = getRandomUsername()
|
||||
names.push(name)
|
||||
i++
|
||||
}
|
||||
|
||||
setRandomNames(names)
|
||||
setIsLoading(false)
|
||||
})
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
setIsLoading(true)
|
||||
InteractionManager.runAfterInteractions(async () => {
|
||||
let i = 0
|
||||
let names = []
|
||||
while (i < 8) {
|
||||
const name = getRandomUsername()
|
||||
names.push(name)
|
||||
i++
|
||||
}
|
||||
load()
|
||||
return () => {}
|
||||
}, [])
|
||||
|
||||
const confirmSelectedName = async function () {
|
||||
if(!selectedName) {
|
||||
setInfo('Select one of the usernames')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
|
||||
await walletProfileStore.updateName(selectedName)
|
||||
|
||||
setIsLoading(false)
|
||||
navigation.goBack()
|
||||
return
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const handleError = function (e: AppError): void {
|
||||
setRandomNames(names)
|
||||
setIsLoading(false)
|
||||
setError(e)
|
||||
})
|
||||
}
|
||||
//
|
||||
return (
|
||||
<Screen contentContainerStyle={$screen}>
|
||||
<View style={$contentContainer}>
|
||||
<Card
|
||||
ContentComponent={
|
||||
<>
|
||||
<FlatList
|
||||
data={randomNames}
|
||||
numColumns={2}
|
||||
renderItem={({ item, index }) => {
|
||||
return(
|
||||
<Button
|
||||
key={index}
|
||||
preset={selectedName === item ? 'default' : 'secondary'}
|
||||
onPress={() => setSelectedName(item)}
|
||||
text={`${item}`}
|
||||
style={{minWidth: scale(150), margin: spacing.extraSmall}}
|
||||
textStyle={$sizeStyles.xs}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
keyExtractor={(item) => item}
|
||||
style={{ flexGrow: 0 }}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
style={$card}
|
||||
/>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
onPress={() => confirmSelectedName()}
|
||||
text={'Confirm'}
|
||||
style={{marginTop: spacing.medium, minWidth: 120}}
|
||||
/>
|
||||
</View>
|
||||
load()
|
||||
return () => {}
|
||||
}, [])
|
||||
|
||||
const confirmSelectedName = async function () {
|
||||
if (!selectedName) {
|
||||
setInfo(translate('contactsScreen.randomName.selectOneOfUsernames'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
|
||||
await walletProfileStore.updateName(selectedName)
|
||||
|
||||
setIsLoading(false)
|
||||
navigation.goBack()
|
||||
return
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
const handleError = function (e: AppError): void {
|
||||
setIsLoading(false)
|
||||
setError(e)
|
||||
}
|
||||
//
|
||||
return (
|
||||
<Screen contentContainerStyle={$screen}>
|
||||
<View style={$contentContainer}>
|
||||
<Card
|
||||
ContentComponent={
|
||||
<FlatList
|
||||
data={randomNames}
|
||||
numColumns={2}
|
||||
renderItem={({item, index}) => {
|
||||
return (
|
||||
<Button
|
||||
key={index}
|
||||
preset={selectedName === item ? 'default' : 'secondary'}
|
||||
onPress={() => setSelectedName(item)}
|
||||
text={`${item}`}
|
||||
style={{minWidth: scale(150), margin: spacing.extraSmall}}
|
||||
textStyle={$sizeStyles.xs}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
keyExtractor={item => item}
|
||||
style={{flexGrow: 0}}
|
||||
/>
|
||||
}
|
||||
style={$card}
|
||||
/>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
onPress={() => confirmSelectedName()}
|
||||
tx="common.confirm"
|
||||
style={{marginTop: spacing.medium, minWidth: 120}}
|
||||
/>
|
||||
</View>
|
||||
{error && <ErrorModal error={error} />}
|
||||
{info && <InfoModal message={info} />}
|
||||
{isLoading && <Loading/>}
|
||||
</Screen>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
</View>
|
||||
{error && <ErrorModal error={error} />}
|
||||
{info && <InfoModal message={info} />}
|
||||
{isLoading && <Loading />}
|
||||
</Screen>
|
||||
)
|
||||
})
|
||||
|
||||
const $screen: ViewStyle = {}
|
||||
|
||||
|
||||
const $contentContainer: TextStyle = {
|
||||
padding: spacing.extraSmall,
|
||||
const $contentContainer: TextStyle = {
|
||||
padding: spacing.extraSmall,
|
||||
}
|
||||
|
||||
const $buttonContainer: ViewStyle = {
|
||||
flexDirection: 'row',
|
||||
alignSelf: 'center',
|
||||
flexDirection: 'row',
|
||||
alignSelf: 'center',
|
||||
}
|
||||
|
||||
const $bottomContainer: ViewStyle = {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
marginBottom: spacing.medium,
|
||||
alignSelf: 'stretch',
|
||||
// opacity: 0,
|
||||
}
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
marginBottom: spacing.medium,
|
||||
alignSelf: 'stretch',
|
||||
// opacity: 0,
|
||||
}
|
||||
|
||||
const $card: ViewStyle = {
|
||||
marginBottom: 0,
|
||||
marginBottom: 0,
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export const IncomingRequests = observer(function (props: {
|
||||
/>
|
||||
) : (
|
||||
<Card
|
||||
content={'There are no incoming payment requests to be paid or they have already expired.'}
|
||||
contentTx='paymentRequestScreen.incoming.noRequests'
|
||||
contentStyle={{color: hintColor, padding: spacing.small}}
|
||||
style={$card}
|
||||
/>
|
||||
|
||||
@@ -35,6 +35,7 @@ import { PaymentRequestListItem } from './PaymentRequestListItem'
|
||||
import QRCode from 'react-native-qrcode-svg'
|
||||
import { infoMessage } from '../../utils/utils'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import { translate } from '../../i18n'
|
||||
|
||||
|
||||
export const OutgoingRequests = observer(function (props: {
|
||||
@@ -79,11 +80,11 @@ export const OutgoingRequests = observer(function (props: {
|
||||
|
||||
if (result.action === Share.sharedAction) {
|
||||
setTimeout(
|
||||
() => infoMessage('Lightning invoice has been shared, waiting to be paid by receiver.'),
|
||||
() => infoMessage(translate("paymentRequestScreen.outgoing.invoiceShared")),
|
||||
500,
|
||||
)
|
||||
} else if (result.action === Share.dismissedAction) {
|
||||
infoMessage('Sharing cancelled')
|
||||
infoMessage(translate("paymentRequestScreen.outgoing.sharingCancelled"))
|
||||
}
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
@@ -95,7 +96,7 @@ export const OutgoingRequests = observer(function (props: {
|
||||
try {
|
||||
Clipboard.setString(selectedRequest?.encodedInvoice as string)
|
||||
} catch (e: any) {
|
||||
setInfo(`Could not copy: ${e.message}`)
|
||||
setInfo(translate("common.copyFailParam", { param: e.message }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +134,7 @@ export const OutgoingRequests = observer(function (props: {
|
||||
|
||||
) : (
|
||||
<Card
|
||||
content={'There are no outgoing payment requests to be paid or they have already expired.'}
|
||||
contentTx="paymentRequestScreen.outgoing.noRequests"
|
||||
contentStyle={{color: hintColor, padding: spacing.small}}
|
||||
style={$card}
|
||||
/>
|
||||
@@ -171,7 +172,7 @@ const ShareAsQRCodeBlock = observer(function (props: {
|
||||
}) {
|
||||
return (
|
||||
<View style={[$bottomModal, {marginHorizontal: spacing.small}]}>
|
||||
<Text text={'Scan and pay to top-up'} />
|
||||
<Text tx="paymentRequestScreen.outgoing.scanAndPay" />
|
||||
<View style={$qrCodeContainer}>
|
||||
<QRCode
|
||||
size={270}
|
||||
@@ -181,7 +182,7 @@ const ShareAsQRCodeBlock = observer(function (props: {
|
||||
</View>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
text="Share"
|
||||
tx="common.share"
|
||||
onPress={props.onShareToApp}
|
||||
style={{marginRight: spacing.medium}}
|
||||
LeftAccessory={() => (
|
||||
@@ -193,10 +194,10 @@ const ShareAsQRCodeBlock = observer(function (props: {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Button preset="secondary" text="Copy" onPress={props.onCopy} />
|
||||
<Button preset="secondary" tx="common.copy" onPress={props.onCopy} />
|
||||
<Button
|
||||
preset="tertiary"
|
||||
text="Close"
|
||||
tx="common.close"
|
||||
onPress={props.toggleQRModal}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -1,207 +1,302 @@
|
||||
import { MINIBITS_NIP05_DOMAIN, MINIBITS_SERVER_API_HOST } from "@env"
|
||||
import differenceInSeconds from "date-fns/differenceInSeconds"
|
||||
import formatDistance from "date-fns/formatDistance"
|
||||
import { observer } from "mobx-react-lite"
|
||||
import React from "react"
|
||||
import { Image, ImageStyle, ScrollView, StyleSheet, TextStyle, View, ViewStyle } from "react-native"
|
||||
import { Button, Card, Icon, ListItem, Screen, Text } from "../../components"
|
||||
import { Contact, ContactType } from "../../models/Contact"
|
||||
import { PaymentRequest, PaymentRequestStatus, PaymentRequestType } from "../../models/PaymentRequest"
|
||||
import { log, NostrClient } from "../../services"
|
||||
import { colors, spacing, typography, useThemeColor } from "../../theme"
|
||||
import { getImageSource } from '../../utils/utils'
|
||||
import { ContactListItem } from "../Contacts/ContactListItem"
|
||||
import { SendOption } from "../SendOptionsScreen"
|
||||
import { CurrencyCode, CurrencySign } from "../Wallet/CurrencySign"
|
||||
|
||||
import {MINIBITS_NIP05_DOMAIN, MINIBITS_SERVER_API_HOST} from '@env'
|
||||
import differenceInSeconds from 'date-fns/differenceInSeconds'
|
||||
import formatDistance from 'date-fns/formatDistance'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import React from 'react'
|
||||
import {
|
||||
Image,
|
||||
ImageStyle,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TextStyle,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {Button, Card, Icon, ListItem, Screen, Text} from '../../components'
|
||||
import {Contact, ContactType} from '../../models/Contact'
|
||||
import {
|
||||
PaymentRequest,
|
||||
PaymentRequestStatus,
|
||||
PaymentRequestType,
|
||||
} from '../../models/PaymentRequest'
|
||||
import {log, NostrClient} from '../../services'
|
||||
import {colors, spacing, typography, useThemeColor} from '../../theme'
|
||||
import {getImageSource} from '../../utils/utils'
|
||||
import {ContactListItem} from '../Contacts/ContactListItem'
|
||||
import {SendOption} from '../SendOptionsScreen'
|
||||
import {CurrencyCode, CurrencySign} from '../Wallet/CurrencySign'
|
||||
import {translate} from '../../i18n'
|
||||
|
||||
export interface PaymentRequestListProps {
|
||||
pr: PaymentRequest
|
||||
isFirst: boolean
|
||||
isFirst: boolean
|
||||
navigation: any
|
||||
onShowQRModal?: any
|
||||
}
|
||||
|
||||
export const PaymentRequestListItem = observer(function (props: PaymentRequestListProps) {
|
||||
|
||||
const { pr, isFirst, navigation, onShowQRModal } = props
|
||||
const hintColor = useThemeColor('textDim')
|
||||
const secToExpiry = differenceInSeconds(pr.expiresAt as Date, new Date())
|
||||
const expiryBg = ( secToExpiry < 0 ? colors.palette.angry500 : secToExpiry < 60 ? colors.palette.orange400 : colors.palette.success300)
|
||||
const separatorColor = useThemeColor('separator')
|
||||
export const PaymentRequestListItem = observer(function (
|
||||
props: PaymentRequestListProps,
|
||||
) {
|
||||
const {pr, isFirst, navigation, onShowQRModal} = props
|
||||
const hintColor = useThemeColor('textDim')
|
||||
const secToExpiry = differenceInSeconds(pr.expiresAt as Date, new Date())
|
||||
const expiryBg =
|
||||
secToExpiry < 0
|
||||
? colors.palette.angry500
|
||||
: secToExpiry < 60
|
||||
? colors.palette.orange400
|
||||
: colors.palette.success300
|
||||
const separatorColor = useThemeColor('separator')
|
||||
|
||||
const onGotoContactDetail = function() {
|
||||
log.trace(pr)
|
||||
navigation.navigate('ContactsNavigator', {
|
||||
screen: 'ContactDetail',
|
||||
params: {contact: pr.type === PaymentRequestType.INCOMING ? pr.contactFrom : pr.contactTo}
|
||||
})
|
||||
const onGotoContactDetail = function () {
|
||||
log.trace(pr)
|
||||
navigation.navigate('ContactsNavigator', {
|
||||
screen: 'ContactDetail',
|
||||
params: {
|
||||
contact:
|
||||
pr.type === PaymentRequestType.INCOMING
|
||||
? pr.contactFrom
|
||||
: pr.contactTo,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const onPressPaymentRequest = function () {
|
||||
if (pr.type === PaymentRequestType.INCOMING) {
|
||||
navigation.navigate('Transfer', {
|
||||
paymentRequest: pr,
|
||||
paymentOption: SendOption.PAY_PAYMENT_REQUEST,
|
||||
})
|
||||
} else {
|
||||
if (onShowQRModal) {
|
||||
onShowQRModal(pr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onPressPaymentRequest = function() {
|
||||
if (pr.type === PaymentRequestType.INCOMING) {
|
||||
navigation.navigate('Transfer', {
|
||||
paymentRequest: pr,
|
||||
paymentOption: SendOption.PAY_PAYMENT_REQUEST
|
||||
})
|
||||
} else {
|
||||
if(onShowQRModal) {
|
||||
onShowQRModal(pr)
|
||||
const dim = useThemeColor('textDim')
|
||||
|
||||
return (
|
||||
<Card
|
||||
HeadingComponent={
|
||||
<View
|
||||
style={[
|
||||
$headerContainer,
|
||||
{borderBottomColor: separatorColor, borderBottomWidth: 1},
|
||||
]}>
|
||||
<Text
|
||||
tx={
|
||||
pr.type === PaymentRequestType.INCOMING
|
||||
? 'paymentCommon.pay'
|
||||
: 'paymentCommon.payMe'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const dim = useThemeColor('textDim')
|
||||
|
||||
return (
|
||||
<Card
|
||||
HeadingComponent={
|
||||
<View style={[$headerContainer, {borderBottomColor: separatorColor, borderBottomWidth: 1}]}>
|
||||
<Text
|
||||
text={pr.type === PaymentRequestType.INCOMING ? 'Pay' : 'Pay me'}
|
||||
/>
|
||||
<Text preset="heading" text={pr.amount.toLocaleString()} />
|
||||
<CurrencySign
|
||||
currencyCode={CurrencyCode.SATS}
|
||||
containerStyle={{marginBottom: spacing.small}}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
ContentComponent={
|
||||
<>
|
||||
{pr.contactFrom && pr.contactTo && (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
marginVertical: spacing.medium,
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
width: 100,
|
||||
}}>
|
||||
<Image
|
||||
style={[
|
||||
$profileIcon,
|
||||
{
|
||||
width: 40,
|
||||
height: pr.contactTo?.isExternalDomain ? 40 : 43,
|
||||
borderRadius: pr.contactTo?.isExternalDomain ? 20 : 0,
|
||||
},
|
||||
]}
|
||||
source={{
|
||||
uri: getImageSource(pr.contactTo?.picture as string),
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
size="xxs"
|
||||
style={{color: dim}}
|
||||
text={pr.contactTo?.name}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
size="xxs"
|
||||
style={{
|
||||
color: dim,
|
||||
textAlign: 'center',
|
||||
marginLeft: 30,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
text="..........."
|
||||
/>
|
||||
<View style={{flexDirection: 'column', alignItems: 'center'}}>
|
||||
<Icon icon="faPaperPlane" size={spacing.medium} color={dim} />
|
||||
<Text
|
||||
size="xxs"
|
||||
style={{color: dim, marginBottom: -10}}
|
||||
text={translate('paymentCommon.paySatsAmountTo', {
|
||||
amount: pr.amount,
|
||||
})}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
size="xxs"
|
||||
style={{
|
||||
color: dim,
|
||||
textAlign: 'center',
|
||||
marginRight: 30,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
text="..........."
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
width: 100,
|
||||
}}>
|
||||
{pr.contactFrom.picture ? (
|
||||
<View style={{borderRadius: 20, overflow: 'hidden'}}>
|
||||
<Image
|
||||
style={[
|
||||
$profileIcon,
|
||||
{
|
||||
width: 40,
|
||||
height: pr.contactFrom.isExternalDomain ? 40 : 43,
|
||||
borderRadius: pr.contactFrom.isExternalDomain
|
||||
? 20
|
||||
: 0,
|
||||
},
|
||||
]}
|
||||
source={{
|
||||
uri: getImageSource(pr.contactFrom.picture as string),
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
preset='heading'
|
||||
text={pr.amount.toLocaleString()}
|
||||
/>
|
||||
<CurrencySign
|
||||
currencyCode={CurrencyCode.SATS}
|
||||
containerStyle={{marginBottom: spacing.small}}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
ContentComponent={
|
||||
<>
|
||||
{pr.contactFrom && pr.contactTo && (
|
||||
<View style={{flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center', marginVertical: spacing.medium}}>
|
||||
<View style={{flexDirection: 'column', alignItems: 'center', width: 100}}>
|
||||
<Image style={[
|
||||
$profileIcon, {
|
||||
width: 40,
|
||||
height: pr.contactTo?.isExternalDomain ? 40 : 43,
|
||||
borderRadius: pr.contactTo?.isExternalDomain ? 20 : 0,
|
||||
}]}
|
||||
source={{
|
||||
uri: getImageSource(pr.contactTo?.picture as string)
|
||||
}}
|
||||
/>
|
||||
<Text size='xxs' style={{color: dim}} text={pr.contactTo?.name}/>
|
||||
</View>
|
||||
<Text size='xxs' style={{color: dim, textAlign: 'center', marginLeft: 30, marginBottom: 20}} text='...........' />
|
||||
<View style={{flexDirection: 'column', alignItems: 'center'}}>
|
||||
<Icon
|
||||
icon='faPaperPlane'
|
||||
size={spacing.medium}
|
||||
color={dim}
|
||||
/>
|
||||
<Text size='xxs' style={{color: dim, marginBottom: -10}} text={`pay ${pr.amount} SATS to`} />
|
||||
</View>
|
||||
<Text size='xxs' style={{color: dim, textAlign: 'center', marginRight: 30, marginBottom: 20}} text='...........' />
|
||||
<View style={{flexDirection: 'column', alignItems: 'center', width: 100}}>
|
||||
{pr.contactFrom.picture ? (
|
||||
<View style={{borderRadius: 20, overflow: 'hidden'}}>
|
||||
<Image style={[
|
||||
$profileIcon, {
|
||||
width: 40,
|
||||
height: pr.contactFrom.isExternalDomain ? 40 : 43,
|
||||
borderRadius: pr.contactFrom.isExternalDomain ? 20 : 0,
|
||||
}]}
|
||||
source={{
|
||||
uri: getImageSource(pr.contactFrom.picture as string)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<Icon
|
||||
icon='faCircleUser'
|
||||
size={38}
|
||||
color={dim}
|
||||
/>
|
||||
)}
|
||||
<Text size='xxs' style={{color: dim}} text={pr.contactFrom.name}/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
{pr.description && (
|
||||
<View style={{borderBottomColor: separatorColor, borderBottomWidth: 1, paddingLeft: 8}}>
|
||||
<ListItem
|
||||
leftIcon="faInfoCircle"
|
||||
text={pr.description}
|
||||
textStyle={{fontSize: 14, marginLeft: 8}}
|
||||
topSeparator={true}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{pr.status === PaymentRequestStatus.ACTIVE ? (
|
||||
<Text
|
||||
style={[$expiry, {backgroundColor: expiryBg}]}
|
||||
text={`Expire${secToExpiry > 0 ? 's' : 'd'} ${formatDistance(pr.expiresAt as Date, new Date(), {addSuffix: true})}`}
|
||||
size='xxs'
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
style={[$expiry, {backgroundColor: colors.palette.success300}]}
|
||||
text={pr.status === PaymentRequestStatus.PAID ? `Request has been paid` : ''}
|
||||
size='xxs'
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
FooterComponent={
|
||||
<View style={$buttonContainer}>
|
||||
{pr.status === PaymentRequestStatus.ACTIVE && (
|
||||
<Button
|
||||
preset="secondary"
|
||||
text={pr.type === PaymentRequestType.INCOMING ? 'Pay from wallet' : 'Receive in person'}
|
||||
onPress={onPressPaymentRequest}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
}
|
||||
style={$card}
|
||||
/>
|
||||
)
|
||||
</View>
|
||||
) : (
|
||||
<Icon icon="faCircleUser" size={38} color={dim} />
|
||||
)}
|
||||
<Text
|
||||
size="xxs"
|
||||
style={{color: dim}}
|
||||
text={pr.contactFrom.name}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
{pr.description && (
|
||||
<View
|
||||
style={{
|
||||
borderBottomColor: separatorColor,
|
||||
borderBottomWidth: 1,
|
||||
paddingLeft: 8,
|
||||
}}>
|
||||
<ListItem
|
||||
leftIcon="faInfoCircle"
|
||||
text={pr.description}
|
||||
textStyle={{fontSize: 14, marginLeft: 8}}
|
||||
topSeparator={true}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{pr.status === PaymentRequestStatus.ACTIVE ? (
|
||||
<Text
|
||||
style={[$expiry, {backgroundColor: expiryBg}]}
|
||||
text={
|
||||
translate(
|
||||
secToExpiry > 0
|
||||
? 'paymentRequestScreen.listItem.expires'
|
||||
: 'paymentRequestScreen.listItem.expired',
|
||||
) +
|
||||
' ' +
|
||||
formatDistance(pr.expiresAt as Date, new Date(), {
|
||||
addSuffix: true,
|
||||
})
|
||||
}
|
||||
size="xxs"
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
style={[$expiry, {backgroundColor: colors.palette.success300}]}
|
||||
text={
|
||||
pr.status === PaymentRequestStatus.PAID
|
||||
? translate("paymentRequestScreen.listItem.requestPaymentSuccess")
|
||||
: ''
|
||||
}
|
||||
size="xxs"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
FooterComponent={
|
||||
<View style={$buttonContainer}>
|
||||
{pr.status === PaymentRequestStatus.ACTIVE && (
|
||||
<Button
|
||||
preset="secondary"
|
||||
tx={
|
||||
pr.type === PaymentRequestType.INCOMING
|
||||
? "paymentCommon.payFromWallet"
|
||||
: 'paymentCommon.receiveInPerson'
|
||||
}
|
||||
onPress={onPressPaymentRequest}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
}
|
||||
style={$card}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
const $profileIcon: ImageStyle = {
|
||||
padding: spacing.medium,
|
||||
padding: spacing.medium,
|
||||
}
|
||||
|
||||
const $headerContainer: TextStyle = {
|
||||
// justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.medium,
|
||||
// marginBottom: spacing.small,
|
||||
// height: spacing.screenHeight * 0.18,
|
||||
}
|
||||
|
||||
const $expiry: ViewStyle = {
|
||||
paddingHorizontal: spacing.small,
|
||||
borderRadius: spacing.tiny,
|
||||
alignSelf: 'center',
|
||||
marginVertical: spacing.small,
|
||||
// justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.medium,
|
||||
// marginBottom: spacing.small,
|
||||
// height: spacing.screenHeight * 0.18,
|
||||
}
|
||||
|
||||
const $expiry: ViewStyle = {
|
||||
paddingHorizontal: spacing.small,
|
||||
borderRadius: spacing.tiny,
|
||||
alignSelf: 'center',
|
||||
marginVertical: spacing.small,
|
||||
}
|
||||
|
||||
const $buttonContainer: ViewStyle = {
|
||||
flexDirection: 'row',
|
||||
alignSelf: 'center',
|
||||
flexDirection: 'row',
|
||||
alignSelf: 'center',
|
||||
}
|
||||
|
||||
|
||||
const $iconContainer: ImageStyle = {
|
||||
padding: spacing.extraSmall,
|
||||
alignSelf: "center",
|
||||
marginRight: spacing.medium,
|
||||
padding: spacing.extraSmall,
|
||||
alignSelf: 'center',
|
||||
marginRight: spacing.medium,
|
||||
}
|
||||
|
||||
const $prText: TextStyle = {
|
||||
overflow: 'hidden',
|
||||
fontSize: 14,
|
||||
overflow: 'hidden',
|
||||
fontSize: 14,
|
||||
}
|
||||
|
||||
const $card: ViewStyle = {
|
||||
marginBottom: spacing.small,
|
||||
marginBottom: spacing.small,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,178 +1,271 @@
|
||||
import formatDistance from "date-fns/formatDistance"
|
||||
import { observer } from "mobx-react-lite"
|
||||
import React from "react"
|
||||
import { ScrollView, TextStyle, View, ViewStyle } from "react-native"
|
||||
import { Button, Icon, ListItem, Screen, Text } from "../../components"
|
||||
import { Transaction, TransactionStatus, TransactionType } from "../../models/Transaction"
|
||||
import { colors, spacing, typography, useThemeColor } from "../../theme"
|
||||
import useIsInternetReachable from "../../utils/useIsInternetReachable"
|
||||
import formatDistance from 'date-fns/formatDistance'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import React from 'react'
|
||||
import {ScrollView, TextStyle, View, ViewStyle} from 'react-native'
|
||||
import {Button, Icon, ListItem, Screen, Text} from '../../components'
|
||||
import {
|
||||
Transaction,
|
||||
TransactionStatus,
|
||||
TransactionType,
|
||||
} from '../../models/Transaction'
|
||||
import {colors, spacing, typography, useThemeColor} from '../../theme'
|
||||
import useIsInternetReachable from '../../utils/useIsInternetReachable'
|
||||
import {translate} from '../../i18n'
|
||||
|
||||
export interface TransactionListProps {
|
||||
transaction: Transaction,
|
||||
isFirst: boolean,
|
||||
isTimeAgoVisible: boolean,
|
||||
gotoTranDetail: any,
|
||||
transaction: Transaction
|
||||
isFirst: boolean
|
||||
isTimeAgoVisible: boolean
|
||||
gotoTranDetail: any
|
||||
}
|
||||
|
||||
export const TransactionListItem = observer(function (props: TransactionListProps) {
|
||||
|
||||
const { transaction: tx, isTimeAgoVisible } = props
|
||||
|
||||
const txReceiveColor = colors.palette.success300
|
||||
const txSendColor = useThemeColor('amount')
|
||||
const txErrorColor = useThemeColor('textDim')
|
||||
const txPendingColor = useThemeColor('textDim')
|
||||
const isInternetReachable = useIsInternetReachable()
|
||||
|
||||
const getText = function(tx: Transaction) {
|
||||
if(tx.noteToSelf) return tx.noteToSelf
|
||||
// if(tx.memo) return tx.memo
|
||||
|
||||
switch(tx.type) {
|
||||
case TransactionType.RECEIVE || TransactionType.RECEIVE_OFFLINE:
|
||||
if(tx.sentFrom) {
|
||||
if(!tx.memo || tx.memo.includes('Sent from Minibits')) {
|
||||
return `From ${tx.sentFrom}`
|
||||
}
|
||||
} else {
|
||||
return tx.memo ? tx.memo : 'You received'
|
||||
}
|
||||
case TransactionType.SEND:
|
||||
return tx.memo ? tx.memo : tx.sentTo ? `Sent to ${tx.sentTo}` : 'You sent'
|
||||
case TransactionType.TOPUP:
|
||||
return tx.memo ? tx.memo : tx.sentFrom ? `Received from ${tx.sentFrom}` : 'You received'
|
||||
case TransactionType.TRANSFER:
|
||||
return tx.memo ? tx.memo : tx.sentTo ? `Paid to ${tx.sentTo}` : 'You paid'
|
||||
default:
|
||||
return 'Unknown transaction'
|
||||
}
|
||||
export const TransactionListItem = observer(function (
|
||||
props: TransactionListProps,
|
||||
) {
|
||||
const {transaction: tx, isTimeAgoVisible} = props
|
||||
|
||||
const txReceiveColor = colors.palette.success300
|
||||
const txSendColor = useThemeColor('amount')
|
||||
const txErrorColor = useThemeColor('textDim')
|
||||
const txPendingColor = useThemeColor('textDim')
|
||||
const isInternetReachable = useIsInternetReachable()
|
||||
|
||||
const getText = function (tx: Transaction) {
|
||||
if (tx.noteToSelf) return tx.noteToSelf
|
||||
// if(tx.memo) return tx.memo
|
||||
|
||||
switch (tx.type) {
|
||||
case TransactionType.RECEIVE || TransactionType.RECEIVE_OFFLINE:
|
||||
if (tx.sentFrom) {
|
||||
if (!tx.memo || tx.memo.includes('Sent from Minibits')) {
|
||||
return translate('transactionCommon.from', {sender: tx.sentFrom})
|
||||
}
|
||||
} else {
|
||||
return tx.memo ? tx.memo : translate('transactionCommon.youReceived')
|
||||
}
|
||||
case TransactionType.SEND:
|
||||
return tx.memo
|
||||
? tx.memo
|
||||
: tx.sentTo
|
||||
? translate('transactionCommon.sentTo', {receiver: tx.sentTo})
|
||||
: translate('transactionCommon.youSent')
|
||||
case TransactionType.TOPUP:
|
||||
return tx.memo
|
||||
? tx.memo
|
||||
: tx.sentFrom
|
||||
? translate('transactionCommon.receivedFrom', {sender: tx.sentFrom})
|
||||
: translate('transactionCommon.youReceived')
|
||||
case TransactionType.TRANSFER:
|
||||
return tx.memo
|
||||
? tx.memo
|
||||
: tx.sentTo
|
||||
? translate('transactionCommon.paidTo', {receiver: tx.sentTo})
|
||||
: translate('transactionCommon.youPaid')
|
||||
default:
|
||||
return translate('transactionCommon.unknown')
|
||||
}
|
||||
|
||||
|
||||
const getSubText = function(tx: Transaction) {
|
||||
|
||||
let distance = formatDistance(tx.createdAt as Date, new Date(), {addSuffix: true})
|
||||
let timeAgo = ''
|
||||
if (isTimeAgoVisible) timeAgo = `${distance} · `
|
||||
|
||||
switch(tx.status) {
|
||||
case TransactionStatus.COMPLETED:
|
||||
return timeAgo + `Completed ${(tx.fee && tx.fee > 0) ? ' · Fee ' + tx.fee : ''}`
|
||||
case TransactionStatus.DRAFT:
|
||||
return timeAgo + `Draft`
|
||||
case TransactionStatus.ERROR:
|
||||
return timeAgo + `Error`
|
||||
case TransactionStatus.PENDING:
|
||||
return timeAgo + `Pending`
|
||||
case TransactionStatus.PREPARED:
|
||||
return timeAgo + `Prepared`
|
||||
case TransactionStatus.PREPARED_OFFLINE:
|
||||
if(isInternetReachable) {
|
||||
return timeAgo + `Tap to redeem`
|
||||
} else {
|
||||
return timeAgo + `Redeem online`
|
||||
}
|
||||
case TransactionStatus.REVERTED:
|
||||
return timeAgo + `Reverted`
|
||||
case TransactionStatus.BLOCKED:
|
||||
return timeAgo + `Blocked`
|
||||
case TransactionStatus.EXPIRED:
|
||||
return timeAgo + `Expired`
|
||||
default:
|
||||
return timeAgo
|
||||
}
|
||||
}
|
||||
|
||||
const getSubText = function (tx: Transaction) {
|
||||
let distance = formatDistance(tx.createdAt as Date, new Date(), {
|
||||
addSuffix: true,
|
||||
})
|
||||
let timeAgo = ''
|
||||
if (isTimeAgoVisible) timeAgo = `${distance} · `
|
||||
|
||||
switch (tx.status) {
|
||||
case TransactionStatus.COMPLETED:
|
||||
return (
|
||||
timeAgo +
|
||||
translate('transactionCommon.status.completedFee', {
|
||||
fee: (tx.fee && tx.fee > 0) ? ' · Fee ' + tx.fee : '',
|
||||
})
|
||||
)
|
||||
case TransactionStatus.DRAFT:
|
||||
return timeAgo + translate('transactionCommon.status.draft')
|
||||
case TransactionStatus.ERROR:
|
||||
return timeAgo + translate('transactionCommon.status.error')
|
||||
case TransactionStatus.PENDING:
|
||||
return timeAgo + translate('transactionCommon.status.pending')
|
||||
case TransactionStatus.PREPARED:
|
||||
return timeAgo + translate('transactionCommon.status.prepared')
|
||||
case TransactionStatus.PREPARED_OFFLINE:
|
||||
if (isInternetReachable) {
|
||||
return timeAgo + translate('transactionCommon.tapToRedeem')
|
||||
} else {
|
||||
return timeAgo + translate('transactionCommon.redeemOnline')
|
||||
}
|
||||
case TransactionStatus.REVERTED:
|
||||
return timeAgo + translate('transactionCommon.status.reverted')
|
||||
case TransactionStatus.BLOCKED:
|
||||
return timeAgo + translate('transactionCommon.status.blocked')
|
||||
case TransactionStatus.EXPIRED:
|
||||
return timeAgo + translate('transactionCommon.status.expired')
|
||||
default:
|
||||
return timeAgo
|
||||
}
|
||||
}
|
||||
|
||||
const getLeftIcon = function (tx: Transaction) {
|
||||
if (
|
||||
[
|
||||
TransactionStatus.ERROR,
|
||||
TransactionStatus.EXPIRED,
|
||||
TransactionStatus.BLOCKED,
|
||||
].includes(tx.status)
|
||||
) {
|
||||
return (
|
||||
<Icon
|
||||
containerStyle={$txIconContainer}
|
||||
icon="faBan"
|
||||
size={spacing.medium}
|
||||
color={txErrorColor}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const getLeftIcon = function(tx: Transaction) {
|
||||
if([TransactionStatus.ERROR, TransactionStatus.EXPIRED, TransactionStatus.BLOCKED].includes(tx.status)) {
|
||||
return (<Icon containerStyle={$txIconContainer} icon="faBan" size={spacing.medium} color={txErrorColor}/>)
|
||||
}
|
||||
|
||||
if([TransactionType.TOPUP].includes(tx.type) && tx.status === TransactionStatus.PENDING) {
|
||||
return (<Icon containerStyle={$txIconContainer} icon="faArrowTurnDown" size={spacing.medium} color={txPendingColor}/>)
|
||||
}
|
||||
|
||||
if([TransactionType.RECEIVE, TransactionType.TOPUP].includes(tx.type)) {
|
||||
return (<Icon containerStyle={$txIconContainer} icon="faArrowTurnDown" size={spacing.medium} color={txReceiveColor}/>)
|
||||
}
|
||||
|
||||
if([TransactionType.RECEIVE_OFFLINE].includes(tx.type)) {
|
||||
return (<Icon containerStyle={$txIconContainer} icon="faArrowTurnDown" size={spacing.medium} color={txPendingColor}/>)
|
||||
}
|
||||
|
||||
return (<Icon containerStyle={$txIconContainer} icon="faArrowTurnUp" size={spacing.medium} color={txSendColor}/>)
|
||||
if (
|
||||
[TransactionType.TOPUP].includes(tx.type) &&
|
||||
tx.status === TransactionStatus.PENDING
|
||||
) {
|
||||
return (
|
||||
<Icon
|
||||
containerStyle={$txIconContainer}
|
||||
icon="faArrowTurnDown"
|
||||
size={spacing.medium}
|
||||
color={txPendingColor}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
if ([TransactionType.RECEIVE, TransactionType.TOPUP].includes(tx.type)) {
|
||||
return (
|
||||
<Icon
|
||||
containerStyle={$txIconContainer}
|
||||
icon="faArrowTurnDown"
|
||||
size={spacing.medium}
|
||||
color={txReceiveColor}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if ([TransactionType.RECEIVE_OFFLINE].includes(tx.type)) {
|
||||
return (
|
||||
<Icon
|
||||
containerStyle={$txIconContainer}
|
||||
icon="faArrowTurnDown"
|
||||
size={spacing.medium}
|
||||
color={txPendingColor}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={tx.id}
|
||||
text={getText(tx)}
|
||||
textStyle={$mintText}
|
||||
subText={getSubText(tx)}
|
||||
LeftComponent={getLeftIcon(tx)}
|
||||
RightComponent={
|
||||
<View style={$txContainer}>
|
||||
{([TransactionType.RECEIVE, TransactionType.RECEIVE_OFFLINE].includes(tx.type)) && (
|
||||
<>
|
||||
{[TransactionStatus.COMPLETED].includes(tx.status) && (
|
||||
<Text style={[$txAmount, {color: txReceiveColor}]}>{tx.amount.toLocaleString()}</Text>
|
||||
)}
|
||||
{[TransactionStatus.ERROR, TransactionStatus.BLOCKED, TransactionStatus.PREPARED_OFFLINE].includes(tx.status) && (
|
||||
<Text style={[$txAmount, {color: txErrorColor}]}>{tx.amount.toLocaleString()}</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{([TransactionType.TOPUP].includes(tx.type)) && (
|
||||
<>
|
||||
{[TransactionStatus.PENDING, TransactionStatus.EXPIRED].includes(tx.status) && (
|
||||
<Text style={[$txAmount, {color: txPendingColor}]}>{tx.amount.toLocaleString()}</Text>
|
||||
)}
|
||||
{tx.status === TransactionStatus.COMPLETED && (
|
||||
<Text style={[$txAmount, {color: txReceiveColor}]}>{tx.amount.toLocaleString()}</Text>
|
||||
)}
|
||||
{tx.status === TransactionStatus.ERROR && (
|
||||
<Text style={[$txAmount, {color: txErrorColor}]}>{tx.amount.toLocaleString()}</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{([TransactionType.SEND, TransactionType.TRANSFER].includes(tx.type)) && (
|
||||
<Text style={[$txAmount, {color: (tx.status === TransactionStatus.ERROR) ? txErrorColor : txSendColor}]}>-{tx.amount.toLocaleString()}</Text>
|
||||
)}
|
||||
</View>
|
||||
}
|
||||
topSeparator={props.isFirst ? false : true}
|
||||
style={$item}
|
||||
onPress={() => props.gotoTranDetail(tx.id)}
|
||||
<Icon
|
||||
containerStyle={$txIconContainer}
|
||||
icon="faArrowTurnUp"
|
||||
size={spacing.medium}
|
||||
color={txSendColor}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={tx.id}
|
||||
text={getText(tx)}
|
||||
textStyle={$mintText}
|
||||
subText={getSubText(tx)}
|
||||
LeftComponent={getLeftIcon(tx)}
|
||||
RightComponent={
|
||||
<View style={$txContainer}>
|
||||
{[TransactionType.RECEIVE, TransactionType.RECEIVE_OFFLINE].includes(
|
||||
tx.type,
|
||||
) && (
|
||||
<>
|
||||
{[TransactionStatus.COMPLETED].includes(tx.status) && (
|
||||
<Text style={[$txAmount, {color: txReceiveColor}]}>
|
||||
{tx.amount.toLocaleString()}
|
||||
</Text>
|
||||
)}
|
||||
{[
|
||||
TransactionStatus.ERROR,
|
||||
TransactionStatus.BLOCKED,
|
||||
TransactionStatus.PREPARED_OFFLINE,
|
||||
].includes(tx.status) && (
|
||||
<Text style={[$txAmount, {color: txErrorColor}]}>
|
||||
{tx.amount.toLocaleString()}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{[TransactionType.TOPUP].includes(tx.type) && (
|
||||
<>
|
||||
{[TransactionStatus.PENDING, TransactionStatus.EXPIRED].includes(
|
||||
tx.status,
|
||||
) && (
|
||||
<Text style={[$txAmount, {color: txPendingColor}]}>
|
||||
{tx.amount.toLocaleString()}
|
||||
</Text>
|
||||
)}
|
||||
{tx.status === TransactionStatus.COMPLETED && (
|
||||
<Text style={[$txAmount, {color: txReceiveColor}]}>
|
||||
{tx.amount.toLocaleString()}
|
||||
</Text>
|
||||
)}
|
||||
{tx.status === TransactionStatus.ERROR && (
|
||||
<Text style={[$txAmount, {color: txErrorColor}]}>
|
||||
{tx.amount.toLocaleString()}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{[TransactionType.SEND, TransactionType.TRANSFER].includes(
|
||||
tx.type,
|
||||
) && (
|
||||
<Text
|
||||
style={[
|
||||
$txAmount,
|
||||
{
|
||||
color:
|
||||
tx.status === TransactionStatus.ERROR
|
||||
? txErrorColor
|
||||
: txSendColor,
|
||||
},
|
||||
]}>
|
||||
-{tx.amount.toLocaleString()}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
}
|
||||
topSeparator={props.isFirst ? false : true}
|
||||
style={$item}
|
||||
onPress={() => props.gotoTranDetail(tx.id)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
const $item: ViewStyle = {
|
||||
marginHorizontal: spacing.micro,
|
||||
}
|
||||
|
||||
|
||||
const $item: ViewStyle = {
|
||||
marginHorizontal: spacing.micro,
|
||||
}
|
||||
|
||||
const $mintText: TextStyle = {
|
||||
overflow: 'hidden',
|
||||
fontSize: 14,
|
||||
}
|
||||
|
||||
const $txContainer: ViewStyle = {
|
||||
justifyContent: 'center',
|
||||
alignSelf: 'center',
|
||||
marginRight: spacing.extraSmall
|
||||
}
|
||||
|
||||
const $txAmount: TextStyle = {
|
||||
fontFamily: typography.primary?.medium
|
||||
}
|
||||
|
||||
const $txIconContainer: ViewStyle = {
|
||||
padding: spacing.extraSmall,
|
||||
alignSelf: "center",
|
||||
marginRight: spacing.medium,
|
||||
}
|
||||
const $mintText: TextStyle = {
|
||||
overflow: 'hidden',
|
||||
fontSize: 14,
|
||||
}
|
||||
|
||||
const $txContainer: ViewStyle = {
|
||||
justifyContent: 'center',
|
||||
alignSelf: 'center',
|
||||
marginRight: spacing.extraSmall,
|
||||
}
|
||||
|
||||
const $txAmount: TextStyle = {
|
||||
fontFamily: typography.primary?.medium,
|
||||
}
|
||||
|
||||
const $txIconContainer: ViewStyle = {
|
||||
padding: spacing.extraSmall,
|
||||
alignSelf: 'center',
|
||||
marginRight: spacing.medium,
|
||||
}
|
||||
|
||||
@@ -1,45 +1,52 @@
|
||||
import React from "react"
|
||||
import { TextStyle, View, ViewStyle } from "react-native"
|
||||
import { Button, Icon, IconTypes, ListItem, Screen, Text } from "../../components"
|
||||
import { colors, spacing, typography, useThemeColor } from "../../theme"
|
||||
import React from 'react'
|
||||
import {TextStyle, View, ViewStyle} from 'react-native'
|
||||
import {Button, Icon, IconTypes, ListItem, Screen, Text} from '../../components'
|
||||
import {colors, spacing, typography, useThemeColor} from '../../theme'
|
||||
import {translate} from '../../i18n'
|
||||
|
||||
export enum CurrencyCode {
|
||||
SATS = 'SATS',
|
||||
SATS = 'SATS',
|
||||
}
|
||||
|
||||
export const FeeBadge = function(props: {
|
||||
currencyCode: CurrencyCode,
|
||||
estimatedFee: number,
|
||||
finalFee: number,
|
||||
containerStyle?: ViewStyle,
|
||||
textStyle?: TextStyle,
|
||||
}
|
||||
) {
|
||||
|
||||
const textColor = useThemeColor('header')
|
||||
const bgColor = colors.palette.primary200
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[{
|
||||
alignSelf: 'center',
|
||||
marginTop: spacing.tiny,
|
||||
paddingHorizontal: spacing.tiny,
|
||||
borderRadius: spacing.tiny,
|
||||
backgroundColor: bgColor,
|
||||
}, props.containerStyle || {}]}
|
||||
>
|
||||
<Text
|
||||
text={props.finalFee ? `+ final fee ${props.finalFee} SATS` : `+ fee up to ${props.estimatedFee} SATS`}
|
||||
style={[{
|
||||
color: textColor,
|
||||
fontSize: 10,
|
||||
fontFamily: typography.primary?.light,
|
||||
padding: 0,
|
||||
lineHeight: 16,
|
||||
}, props.textStyle || {}]}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
export const FeeBadge = function (props: {
|
||||
currencyCode: CurrencyCode
|
||||
estimatedFee: number
|
||||
finalFee: number
|
||||
containerStyle?: ViewStyle
|
||||
textStyle?: TextStyle
|
||||
}) {
|
||||
const textColor = useThemeColor('header')
|
||||
const bgColor = colors.palette.primary200
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
alignSelf: 'center',
|
||||
marginTop: spacing.tiny,
|
||||
paddingHorizontal: spacing.tiny,
|
||||
borderRadius: spacing.tiny,
|
||||
backgroundColor: bgColor,
|
||||
},
|
||||
props.containerStyle || {},
|
||||
]}>
|
||||
<Text
|
||||
text={
|
||||
props.finalFee
|
||||
? translate('walletScreen.feeBadge.final', {fee: props.finalFee})
|
||||
: translate('walletScreen.feeBadge.upto', {fee: props.estimatedFee})
|
||||
}
|
||||
style={[
|
||||
{
|
||||
color: textColor,
|
||||
fontSize: 10,
|
||||
fontFamily: typography.primary?.light,
|
||||
padding: 0,
|
||||
lineHeight: 16,
|
||||
},
|
||||
props.textStyle || {},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user