Simplified payment navigation, fix profile sharing

This commit is contained in:
minibits-cash
2023-10-31 01:20:00 +01:00
parent 8a5f6db81e
commit 2ee45fa1a4
10 changed files with 181 additions and 212 deletions
+1
View File
@@ -0,0 +1 @@
export const ScanIcon = '<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"><g id="XMLID_1_"> <path id="XMLID_7_" d="M59.3,93.2c0-8.6,6-14.6,14.6-14.6h83.3V19.3H75.6C34.4,19.3,0,52,0,93.2v83.3h59.3V93.2z M437.3,19.3h-83.3 v59.3h82.5c8.6,0,15.5,6,15.5,14.6v83.3h59.3V93.2C512,52,478.5,19.3,437.3,19.3z M452.7,416.2c0,8.6-6,14.6-14.6,14.6h-83.3V491 h83.3c41.2,0,73.9-34.4,73.9-75.6v-82.5h-59.3V416.2z M73.9,431.7c-8.6,0-14.6-6-14.6-14.6v-82.5H0v82.5c0,41.2,34.4,75.6,75.6,75.6h82.5v-60.1H73.9V431.7z"/></g></svg>'
+1
View File
@@ -13,3 +13,4 @@ export * from "./ErrorModal"
export * from "./InfoModal"
export * from "./BottomModal"
export * from "./Loading"
export * from "./ScanIcon"
+8 -8
View File
@@ -62,12 +62,12 @@ const en = {
received: "Amount received:",
sharePaymentRequest: "Send payment request",
sharePaymentRequestDescription: "Send payment request to one of your contacts, so that the payer can pay you exact amount.",
scanToReceive: "Scan ecash token",
scanToReceiveDescription: "Scan ecash token to receive it into your wallet.",
scanToReceive: "Scan or paste to receive",
scanToReceiveDescription: "Scan or paste ecash token or lnurl withdraw link to receive into your wallet.",
pasteToReceive: "Paste",
pasteoReceiveDescription: "Paste ecash token to receive it into your wallet.",
showOrShareInvoice: "Create lightning invoice",
showOrShareInvoiceDescription: "Present or share the lightning invoice for an amount you want to receive." ,
showOrShareInvoice: "Share lightning invoice",
showOrShareInvoiceDescription: "Present or share lightning invoice for an amount you want to receive." ,
scanWithdrawalLink: "Scan withdrawal link"
},
transferScreen: {
@@ -80,12 +80,12 @@ const en = {
title: "Send",
sendToContact: "Send to contact",
sendToContactDescription: "Send your ecash to one of your contacts stored in your contact list.",
scanToSend: "Scan lightning invoice",
scanToSendDescription: "Scan invoice, LNURL pay code or lightning address to pay to from your wallet.",
scanToSend: "Scan or paste to send",
scanToSendDescription: "Scan or paste invoice, lnurl pay code or lightning address to pay from your wallet.",
pasteToSend: "Paste",
pasteToSendDescription: "Paste lightning invoice from clipboard to pay it from your wallet.",
showOrShareToken: "Share ecash token",
showOrShareTokenDescription: "Present or share ecash for an amount you want to send.",
showOrShareToken: "Share ecash",
showOrShareTokenDescription: "Present or share ecash token for an amount you want to send.",
},
tranDetailScreen: {
amount: "Amount (in satoshis)",
+8 -9
View File
@@ -3,7 +3,7 @@ import React, {FC, useCallback, useEffect, useState} from 'react'
import {ColorValue, Share, TextStyle, View, ViewStyle} from 'react-native'
import {colors, spacing, useThemeColor} from '../theme'
import {ContactsStackScreenProps} from '../navigation'
import {Icon, ListItem, Screen, Text, Card, BottomModal, Button, InfoModal, ErrorModal, Loading} from '../components'
import {Icon, ListItem, Screen, Text, Card, BottomModal, Button, InfoModal, ErrorModal, Loading, Header} from '../components'
import {useHeader} from '../utils/useHeader'
import {useStores} from '../models'
import AppError from '../utils/AppError'
@@ -19,13 +19,6 @@ interface ProfileScreenProps extends ContactsStackScreenProps<'Profile'> {}
export const ProfileScreen: FC<ProfileScreenProps> = observer(
function ProfileScreen({navigation}) {
useHeader({
leftIcon: 'faArrowLeft',
onLeftPress: () => navigation.goBack(),
rightIcon: 'faShareFromSquare',
onRightPress: () => onShareContact()
})
const {walletProfileStore, userSettingsStore} = useStores()
const {npub, name, picture, nip05} = walletProfileStore
@@ -116,7 +109,13 @@ export const ProfileScreen: FC<ProfileScreenProps> = observer(
const iconNpub = useThemeColor('textDim')
return (
<Screen contentContainerStyle={$screen} preset='auto'>
<Screen contentContainerStyle={$screen} preset='auto'>
<Header
leftIcon='faArrowLeft'
onLeftPress={navigation.goBack}
rightIcon='faShareFromSquare'
onRightPress={onShareContact}
/>
<ProfileHeader />
<View style={$contentContainer}>
{!walletProfileStore.isOwnProfile && (
+27 -49
View File
@@ -3,18 +3,17 @@ import React, {FC, useState, useCallback} from 'react'
import {useFocusEffect} from '@react-navigation/native'
import {Alert, TextStyle, View, ViewStyle} from 'react-native'
import Clipboard from '@react-native-clipboard/clipboard'
import { SvgXml } from 'react-native-svg'
import {spacing, useThemeColor, colors} from '../theme'
import {WalletStackScreenProps} from '../navigation'
import {
Button,
Icon,
Card,
Screen,
InfoModal,
ErrorModal,
ListItem,
BottomModal,
Text,
ScanIcon,
} from '../components'
import {useHeader} from '../utils/useHeader'
import {log} from '../utils/logger'
@@ -57,31 +56,8 @@ export const ReceiveOptionsScreen: FC<WalletStackScreenProps<'ReceiveOptions'>>
}
const onScanToken = async function () {
navigation.navigate('Scan', {expectedType: IncomingDataType.CASHU})
}
const onScanLnurl = async function () {
navigation.navigate('Scan', {expectedType: IncomingDataType.LNURL})
}
const onPaste = async function () {
const clipboard = await Clipboard.getString()
if (!clipboard) {
infoMessage('Please copy the ecash token first.')
}
try {
const incomingData = IncomingParser.findAndExtract(clipboard, IncomingDataType.CASHU)
infoMessage('Found ecash token in the clipboard.')
setTimeout(() => navigation.navigate('Receive', {encodedToken: incomingData.encoded}), 1000)
return
} catch (e: any) {
handleError(e)
}
const onScan = async function () {
navigation.navigate('Scan', {})
}
@@ -121,39 +97,41 @@ export const ReceiveOptionsScreen: FC<WalletStackScreenProps<'ReceiveOptions'>>
tx="receiveScreen.showOrShareInvoice"
subTx="receiveScreen.showOrShareInvoiceDescription"
leftIcon='faBolt'
leftIconColor={colors.palette.accent300}
leftIconColor={colors.palette.orange200}
leftIconInverse={true}
style={$item}
onPress={gotoTopup}
/>
<Button
onPress={onScanLnurl}
tx='receiveScreen.scanWithdrawalLink'
preset='secondary'
textStyle={{fontSize: 14, color: iconColor}}
style={{alignSelf: 'flex-start', marginLeft: 43, minHeight: 25, paddingVertical: spacing.extraSmall, marginBottom: spacing.small}}
/>
<ListItem
tx="receiveScreen.scanToReceive"
subTx="receiveScreen.scanToReceiveDescription"
leftIcon='faQrcode'
leftIconColor={colors.palette.success200}
leftIconInverse={true}
LeftComponent={
<View
style={{
marginEnd: spacing.small,
flex: 0,
borderRadius: spacing.small,
padding: spacing.extraSmall,
backgroundColor: colors.palette.success200
}}
>
<SvgXml
width={spacing.medium}
height={spacing.medium}
xml={ScanIcon}
fill='white'
/>
</View>
}
// leftIcon='faQrcode'
// leftIconColor={colors.palette.success200}
// leftIconInverse={true}
style={$item}
topSeparator={true}
onPress={onScanToken}
onPress={onScan}
/>
</>
}
FooterComponent={
<Button
onPress={onPaste}
tx='receiveScreen.pasteToReceive'
preset='secondary'
textStyle={{fontSize: 14, color: iconColor}}
style={{alignSelf: 'flex-start', marginLeft: 43, minHeight: 25, paddingVertical: spacing.extraSmall, marginBottom: spacing.small}}
/>
}
/>
</View>
{info && <InfoModal message={info} />}
+77 -29
View File
@@ -3,17 +3,20 @@ import {
Platform,
PermissionsAndroid,
Alert,
ViewStyle,
View,
} from 'react-native'
import {WalletStackScreenProps} from '../navigation'
import {CameraScreen} from 'react-native-camera-kit'
import {typography} from '../theme'
import {CameraScreen, CameraType} from 'react-native-camera-kit'
import {spacing, typography} from '../theme'
import {useHeader} from '../utils/useHeader'
import {log} from '../utils/logger'
import { IncomingDataType, IncomingParser } from '../services/incomingParser'
import AppError from '../utils/AppError'
import { ErrorModal } from '../components'
import { Button, ErrorModal } from '../components'
import { LnurlUtils } from '../services/lnurl/lnurlUtils'
import { infoMessage } from '../utils/utils'
import Clipboard from '@react-native-clipboard/clipboard'
const hasAndroidCameraPermission = async () => {
const cameraPermission = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA)
@@ -56,48 +59,57 @@ export const ScanScreen: FC<WalletStackScreenProps<'Scan'>> = function ScanScree
}, [route.params?.expectedType])
const onReadCode = async function(event: any) {
setIsScanned(true)
const scanned = event.nativeEvent.codeStringValue
log.trace('Scanned', scanned)
return onIncomingData(scanned)
}
const onIncomingData = async function(incoming: any) {
const routes = navigation.getState()?.routes
const prevRouteName = routes[routes.length - 2].name
log.trace('prevRouteName', prevRouteName)
switch (prevRouteName) {
case 'ReceiveOptions':
log.trace('ReceiveOptions')
try {
if(expected === IncomingDataType.CASHU) {
const tokenResult = IncomingParser.findAndExtract(scanned, IncomingDataType.CASHU)
log.trace('Got token')
return IncomingParser.navigateWithIncomingData(tokenResult, navigation)
}
try {
const tokenResult = IncomingParser.findAndExtract(incoming, IncomingDataType.CASHU)
return IncomingParser.navigateWithIncomingData(tokenResult, navigation)
if(expected === IncomingDataType.LNURL) {
const lnurlResult = IncomingParser.findAndExtract(scanned, IncomingDataType.LNURL)
log.trace('Got LNURL')
await IncomingParser.navigateWithIncomingData(lnurlResult, navigation)
return
}
} catch (e: any) {
e.params = scanned
const maybeLnurl = LnurlUtils.findEncodedLnurl(incoming)
if(maybeLnurl) {
log.trace('Found LNURL link instead of a token', maybeLnurl, 'onIncomingData')
const encodedLnurl = LnurlUtils.extractEncodedLnurl(maybeLnurl)
if(encodedLnurl) {
infoMessage('Found LNURL link in the clipboard.')
return setTimeout(async() => IncomingParser.navigateWithIncomingData({
type: IncomingDataType.LNURL,
encoded: encodedLnurl
}, navigation), 500)
}
return
}
e.params = incoming
handleError(e)
}
case 'SendOptions':
try {
const invoiceResult = IncomingParser.findAndExtract(scanned, IncomingDataType.INVOICE)
log.trace('Got invoice')
const invoiceResult = IncomingParser.findAndExtract(incoming, IncomingDataType.INVOICE)
return IncomingParser.navigateWithIncomingData(invoiceResult, navigation)
} catch (e: any) {
const maybeLnurl = LnurlUtils.findEncodedLnurl(scanned)
const maybeLnurl = LnurlUtils.findEncodedLnurl(incoming)
if(maybeLnurl) {
log.trace('Found LNURL link instead of an invoice', maybeLnurl, 'onPaste')
log.trace('Found LNURL link instead of an invoice', maybeLnurl, 'onIncomingData')
const encodedLnurl = LnurlUtils.extractEncodedLnurl(maybeLnurl)
if(encodedLnurl) {
@@ -110,10 +122,10 @@ export const ScanScreen: FC<WalletStackScreenProps<'Scan'>> = function ScanScree
return
}
const maybeLnurlAddress = LnurlUtils.findEncodedLnurlAddress(scanned)
const maybeLnurlAddress = LnurlUtils.findEncodedLnurlAddress(incoming)
if(maybeLnurlAddress) {
log.trace('Found Lightning address instead of an invoice', maybeLnurlAddress, 'onPaste')
log.trace('Found Lightning address instead of an invoice', maybeLnurlAddress, 'onIncomingData')
const lnurlAddress = LnurlUtils.extractLnurlAddress(maybeLnurlAddress)
if(lnurlAddress) {
@@ -126,22 +138,31 @@ export const ScanScreen: FC<WalletStackScreenProps<'Scan'>> = function ScanScree
return
}
e.params = scanned
e.params = incoming
handleError(e)
break
}
default:
try {
// generic scan button on wallet screen
const incomingData = IncomingParser.findAndExtract(scanned)
const incomingData = IncomingParser.findAndExtract(incoming)
return IncomingParser.navigateWithIncomingData(incomingData, navigation)
} catch (e: any) {
e.params = scanned
e.params = incoming
handleError(e)
}
}
}
}
const onPaste = async function() {
const clipboard = await Clipboard.getString()
if (!clipboard) {
infoMessage('Please copy the invoice first.')
}
return onIncomingData(clipboard)
}
/* const handleError = (scanned: string, message: string) => {
@@ -161,14 +182,41 @@ export const ScanScreen: FC<WalletStackScreenProps<'Scan'>> = function ScanScree
return (shouldLoad ? (
<>
<CameraScreen
actions={{rightButtonText: 'Done', leftButtonText: 'Cancel'}}
cameraType={CameraType.Back}
scanBarcode
onReadCode={event => (isScanned ? undefined : onReadCode(event))}
hideControls
/>
<View style={$bottomContainer}>
<View style={$buttonContainer}>
<Button
onPress={() => onPaste()}
text={'Paste from clipboard'}
preset='secondary'
style={{marginTop: spacing.medium, minWidth: 120}}
/>
</View>
</View>
{error && <ErrorModal error={error} />}
</>
) : null
)
}
const $buttonContainer: ViewStyle = {
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,
}
+32 -71
View File
@@ -15,6 +15,7 @@ import {
ListItem,
BottomModal,
Text,
ScanIcon,
} from '../components'
import {useHeader} from '../utils/useHeader'
import {log} from '../utils/logger'
@@ -23,6 +24,7 @@ import useIsInternetReachable from '../utils/useIsInternetReachable'
import { infoMessage } from '../utils/utils'
import { IncomingDataType, IncomingParser } from '../services/incomingParser'
import { LnurlUtils } from '../services/lnurl/lnurlUtils'
import { SvgXml } from 'react-native-svg'
export enum SendOption {
SEND_TOKEN = 'SEND_TOKEN',
@@ -63,58 +65,6 @@ export const SendOptionsScreen: FC<WalletStackScreenProps<'SendOptions'>> = obse
}
const onPaste = async function () {
const clipboard = await Clipboard.getString()
if (!clipboard) {
infoMessage('Please copy the invoice first.')
}
try {
const incomingData = IncomingParser.findAndExtract(clipboard, IncomingDataType.INVOICE)
infoMessage('Found lightning invoice in the clipboard.')
setTimeout(async() => IncomingParser.navigateWithIncomingData(incomingData, navigation), 500)
return
} catch (e: any) {
const maybeLnurl = LnurlUtils.findEncodedLnurl(clipboard)
if(maybeLnurl) {
log.trace('Found LNURL link instead of an invoice', maybeLnurl, 'onPaste')
const encodedLnurl = LnurlUtils.extractEncodedLnurl(maybeLnurl)
if(encodedLnurl) {
infoMessage('Found LNURL link in the clipboard.')
return setTimeout(async() => IncomingParser.navigateWithIncomingData({
type: IncomingDataType.LNURL,
encoded: encodedLnurl
}, navigation), 500)
}
return
}
const maybeLnurlAddress = LnurlUtils.findEncodedLnurlAddress(clipboard)
if(maybeLnurlAddress) {
log.trace('Found Lightning address instead of an invoice', maybeLnurlAddress, 'onPaste')
const lnurlAddress = LnurlUtils.extractLnurlAddress(maybeLnurlAddress)
if(lnurlAddress) {
infoMessage('Found Lightning address in the clipboard.')
return setTimeout(async() => IncomingParser.navigateWithIncomingData({
type: IncomingDataType.LNURL_ADDRESS,
encoded: lnurlAddress
}, navigation), 500)
}
return
}
e.params = clipboard
handleError(e)
}
}
const handleError = function (e: AppError): void {
log.error(e.name, e.message)
setError(e)
@@ -146,33 +96,44 @@ export const SendOptionsScreen: FC<WalletStackScreenProps<'SendOptions'>> = obse
style={$item}
bottomSeparator={true}
onPress={gotoContacts}
/>
<ListItem
tx="sendScreen.scanToSend"
subTx="sendScreen.scanToSendDescription"
leftIcon='faBolt'
leftIconColor={colors.palette.accent300}
leftIconInverse={true}
style={$item}
onPress={onScan}
/>
<Button
onPress={onPaste}
tx='sendScreen.pasteToSend'
preset='secondary'
textStyle={{fontSize: 14, color: iconColor}}
style={{alignSelf: 'flex-start', marginLeft: 43, minHeight: 25, paddingVertical: spacing.extraSmall, marginBottom: spacing.small}}
/>
/>
<ListItem
tx="sendScreen.showOrShareToken"
subTx="sendScreen.showOrShareTokenDescription"
leftIcon='faQrcode'
leftIconColor={colors.palette.success200}
leftIconColor={colors.palette.iconViolet300}
leftIconInverse={true}
style={$item}
topSeparator={true}
onPress={gotoSend}
/>
<ListItem
tx="sendScreen.scanToSend"
subTx="sendScreen.scanToSendDescription"
LeftComponent={
<View
style={{
marginEnd: spacing.small,
flex: 0,
borderRadius: spacing.small,
padding: spacing.extraSmall,
backgroundColor: colors.palette.success200
}}
>
<SvgXml
width={spacing.medium}
height={spacing.medium}
xml={ScanIcon}
fill='white'
/>
</View>
}
// leftIcon='faBolt'
// leftIconColor={colors.palette.success200}
// leftIconInverse={true}
topSeparator={true}
style={$item}
onPress={onScan}
/>
</>
}
+13 -41
View File
@@ -1,5 +1,5 @@
import {observer} from 'mobx-react-lite'
import React, {FC, useState, useEffect, useCallback, useRef, useMemo, createRef, RefObject} from 'react'
import React, {FC, useState, useEffect, useCallback, useRef} from 'react'
import {useFocusEffect} from '@react-navigation/native'
import {
TextStyle,
@@ -10,18 +10,15 @@ import {
Image,
InteractionManager,
Animated,
findNodeHandle,
FlatList,
Pressable,
Linking
} from 'react-native'
/* import Animated, {
useAnimatedScrollHandler,
useSharedValue,
useAnimatedStyle,
} from 'react-native-reanimated' */
import codePush, { RemotePackage } from 'react-native-code-push'
import {verticalScale} from '@gocodingnow/rn-size-matters'
import { SvgXml } from 'react-native-svg'
import PagerView, { PagerViewOnPageScrollEventData } from 'react-native-pager-view'
import { ScalingDot } from 'react-native-animated-pagination-dots'
import {useThemeColor, spacing, colors, typography} from '../theme'
import {
Button,
@@ -35,12 +32,11 @@ import {
BottomModal,
ErrorModal,
Header,
ScanIcon
} from '../components'
import {useStores} from '../models'
import EventEmitter from '../utils/eventEmitter'
import {WalletStackScreenProps} from '../navigation'
// import useIsInternetReachable from '../utils/useIsInternetReachable'
import {useHeader} from '../utils/useHeader'
import {Mint, MintBalance} from '../models/Mint'
import {MintsByHostname} from '../models/MintsStore'
import {Env, log} from '../utils/logger'
@@ -49,7 +45,6 @@ import {TransactionListItem} from './Transactions/TransactionListItem'
import {MintClient, MintKeys, NostrClient, ReceivedEventResult, Wallet} from '../services'
import {translate} from '../i18n'
import AppError, { Err } from '../utils/AppError'
import { ResultModalInfo } from './Wallet/ResultModalInfo'
import {
APP_ENV,
CODEPUSH_STAGING_DEPLOYMENT_KEY,
@@ -57,14 +52,14 @@ import {
} from '@env'
import { round } from '../utils/number'
import { NotificationService } from '../services/notificationService'
import PagerView, { PagerViewOnPageScrollEventData } from 'react-native-pager-view'
import { ExpandingDot, ScalingDot, SlidingBorder, SlidingDot } from 'react-native-animated-pagination-dots'
import { PaymentRequest, PaymentRequestStatus } from '../models/PaymentRequest'
import { Invoice } from '../models/Invoice'
import { poller } from '../utils/poller'
import Clipboard from '@react-native-clipboard/clipboard'
import { IncomingDataType, IncomingParser } from '../services/incomingParser'
const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
const deploymentKey = APP_ENV === Env.PROD ? CODEPUSH_PRODUCTION_DEPLOYMENT_KEY : CODEPUSH_STAGING_DEPLOYMENT_KEY
@@ -498,10 +493,11 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
/>
<Button
RightAccessory={() => (
<Icon
icon='faExpand'
color='white'
size={spacing.medium}
<SvgXml
width={spacing.medium}
height={spacing.medium}
xml={ScanIcon}
fill='white'
/>
)}
onPress={gotoScan}
@@ -657,31 +653,7 @@ const MintsByHostnameListItem = observer(function (props: {
)
})
/* const LightningActionsBlock = function (props: {
gotoWithdraw: any
gotoTopup: any
}) {
return (
<>
<ListItem
tx='walletScreen.topUpWallet'
subTx='walletScreen.topUpWalletSubText'
leftIcon='faArrowRightToBracket'
leftIconTransform='rotate-90'
onPress={props.gotoTopup}
bottomSeparator={true}
style={{paddingHorizontal: spacing.medium}}
/>
<ListItem
tx='walletScreen.transferFromWallet'
subTx='walletScreen.transferFromWalletSubText'
leftIcon='faArrowUpFromBracket'
onPress={props.gotoWithdraw}
style={{paddingHorizontal: spacing.medium}}
/>
</>
)
} */
const $screen: ViewStyle = {
flex: 1,
+12 -5
View File
@@ -43,12 +43,19 @@ const extractEncodedLightningInvoice = function (maybeInvoice: string) {
}
invoice = decodeInvoice(encodedInvoice) // throws
return encodedInvoice
} else {
invoice = decodeInvoice(maybeInvoice) // throws
return maybeInvoice
return encodedInvoice
}
if (maybeInvoice.startsWith('bitcoin:')) {
const url = new URL(maybeInvoice)
// Use URLSearchParams to get the value of the "lightning" parameter
encodedInvoice = url.searchParams.get("lightning") as string
invoice = decodeInvoice(encodedInvoice) // throws
return encodedInvoice
}
invoice = decodeInvoice(maybeInvoice) // throws
return maybeInvoice
}
+2
View File
@@ -45,6 +45,7 @@ const palette = {
blue200: '#818AFF',
orange200: '#FFAB51',
orange400: '#F7931A',
focus100: '#FB4E9E',
focus200: '#F62586',
@@ -58,6 +59,7 @@ const palette = {
iconGreyBlue400: '#0E629B',
iconMagenta200: '#F62586',
iconViolet200: '#AF00C9',
iconViolet300: '#7F38CA',
} as const
export const colors = {