Skip to content

Commit

Permalink
chore: move nfc to receive screen (#2712)
Browse files Browse the repository at this point in the history
* chore: move nfc to receive screen

* feat: input amount before nfc tag is read

* test: added mock to receive bitcoin

* fix: remove lnrul withdraw intermediary page

* chode: dev codegen

* chore: comment on exhaustive deps

* fix: dont throw error
  • Loading branch information
sandipndev authored Oct 19, 2023
1 parent fc67c11 commit f55ccf6
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 66 deletions.
9 changes: 9 additions & 0 deletions __tests__/screens/receive.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ import { act, render } from "@testing-library/react-native"
import { ContextForScreen } from "./helper"
import ReceiveScreen from "@app/screens/receive-bitcoin-screen/receive-screen"

jest.mock("react-native-nfc-manager", () => {
return {
NfcManager: {
start: jest.fn(),
stop: jest.fn(),
},
}
})

it("Receive", async () => {
render(
<ContextForScreen>
Expand Down
78 changes: 53 additions & 25 deletions app/components/modal-nfc/modal-nfc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,35 @@ import Icon from "react-native-vector-icons/Ionicons"
import { GaloySecondaryButton } from "../atomic/galoy-secondary-button"
import { parseDestination } from "@app/screens/send-bitcoin-screen/payment-destination"
import { logParseDestinationResult } from "@app/utils/analytics"
import { useNavigation } from "@react-navigation/native"
import { StackNavigationProp } from "@react-navigation/stack"
import { RootStackParamList } from "@app/navigation/stack-param-lists"
import { DestinationDirection } from "@app/screens/send-bitcoin-screen/payment-destination/index.types"
import {
DestinationDirection,
ReceiveDestination,
} from "@app/screens/send-bitcoin-screen/payment-destination/index.types"
import {
WalletCurrency,
useAccountDefaultWalletLazyQuery,
useScanningQrCodeScreenQuery,
} from "@app/graphql/generated"
import { useIsAuthed } from "@app/graphql/is-authed-context"
import { LNURL_DOMAINS } from "@app/config"
import { isIOS } from "@rneui/base"
import {
MoneyAmount,
WalletAmount,
toBtcMoneyAmount,
toUsdMoneyAmount,
} from "@app/types/amounts"
import { usePriceConversion } from "@app/hooks"

export const ModalNfc: React.FC<{
isActive: boolean
setIsActive: (arg: boolean) => void
}> = ({ isActive, setIsActive }) => {
settlementAmount?: WalletAmount<WalletCurrency>
receiveViaNFC: (
destination: ReceiveDestination,
settlementAmount: MoneyAmount<"BTC">,
) => Promise<void>
}> = ({ isActive, setIsActive, settlementAmount, receiveViaNFC }) => {
const { data } = useScanningQrCodeScreenQuery({ skip: !useIsAuthed() })
const wallets = data?.me?.defaultAccount.wallets
const bitcoinNetwork = data?.globals?.network
Expand All @@ -33,10 +46,6 @@ export const ModalNfc: React.FC<{
fetchPolicy: "no-cache",
})

// FIXME: navigation destination?
const navigation =
useNavigation<StackNavigationProp<RootStackParamList, "sendBitcoinDestination">>()

const styles = useStyles()
const {
theme: { colors },
Expand All @@ -49,8 +58,23 @@ export const ModalNfc: React.FC<{
NfcManager.cancelTechnologyRequest()
}, [setIsActive])

const { convertMoneyAmount } = usePriceConversion()

React.useEffect(() => {
if (!LL || !wallets || !bitcoinNetwork || !isActive) {
if (isActive && !settlementAmount) {
Alert.alert(LL.ReceiveScreen.enterAmountFirst())
setIsActive(false)
return
}

if (
!LL ||
!wallets ||
!bitcoinNetwork ||
!isActive ||
!receiveViaNFC ||
!settlementAmount
) {
return
}

Expand Down Expand Up @@ -122,38 +146,42 @@ export const ModalNfc: React.FC<{
})
logParseDestinationResult(destination)

if (destination.valid) {
if (destination.valid && settlementAmount && convertMoneyAmount) {
if (destination.destinationDirection === DestinationDirection.Send) {
Alert.alert(LL.SettingsScreen.nfcOnlyReceive())
} else {
navigation.reset({
routes: [
{
name: "Primary",
},
{
name: "redeemBitcoinDetail",
params: {
receiveDestination: destination,
},
},
],
})
let amount = settlementAmount.amount
if (settlementAmount.currency === WalletCurrency.Usd) {
amount = convertMoneyAmount(
toUsdMoneyAmount(settlementAmount.amount),
WalletCurrency.Btc,
).amount
}

destination.validDestination.minWithdrawable = amount * 1000 // coz msats
destination.validDestination.maxWithdrawable = amount * 1000 // coz msats

receiveViaNFC(destination, toBtcMoneyAmount(settlementAmount.amount))
}
}

dismiss()
}

init()
// Necessary because receiveViaNFC gets rerendered at useReceiveBitcoin
// And rerendering that shouldn't cause this useEffect to retrigger
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
LL,
wallets,
bitcoinNetwork,
accountDefaultWalletQuery,
navigation,
isActive,
dismiss,
settlementAmount,
setIsActive,
convertMoneyAmount,
])

return (
Expand Down
1 change: 1 addition & 0 deletions app/i18n/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2084,6 +2084,7 @@ const en: BaseTranslation = {
title: "Home",
},
ReceiveScreen: {
enterAmountFirst: "Please enter an amount first",
activateNotifications:
"Do you want to activate notifications to be notified when the payment has arrived?",
copyClipboard: "Invoice has been copied in the clipboard",
Expand Down
8 changes: 8 additions & 0 deletions app/i18n/i18n-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6468,6 +6468,10 @@ type RootTranslation = {
title: string
}
ReceiveScreen: {
/**
* P​l​e​a​s​e​ ​e​n​t​e​r​ ​a​n​ ​a​m​o​u​n​t​ ​f​i​r​s​t
*/
enterAmountFirst: string
/**
* D​o​ ​y​o​u​ ​w​a​n​t​ ​t​o​ ​a​c​t​i​v​a​t​e​ ​n​o​t​i​f​i​c​a​t​i​o​n​s​ ​t​o​ ​b​e​ ​n​o​t​i​f​i​e​d​ ​w​h​e​n​ ​t​h​e​ ​p​a​y​m​e​n​t​ ​h​a​s​ ​a​r​r​i​v​e​d​?
*/
Expand Down Expand Up @@ -15085,6 +15089,10 @@ export type TranslationFunctions = {
title: () => LocalizedString
}
ReceiveScreen: {
/**
* Please enter an amount first
*/
enterAmountFirst: () => LocalizedString
/**
* Do you want to activate notifications to be notified when the payment has arrived?
*/
Expand Down
1 change: 1 addition & 0 deletions app/i18n/raw-i18n/source/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2055,6 +2055,7 @@
"title": "Home"
},
"ReceiveScreen": {
"enterAmountFirst": "Please enter an amount first",
"activateNotifications": "Do you want to activate notifications to be notified when the payment has arrived?",
"copyClipboard": "Invoice has been copied in the clipboard",
"copyClipboardBitcoin": "Bitcoin address has been copied in the clipboard",
Expand Down
30 changes: 15 additions & 15 deletions app/navigation/root-navigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,21 +81,6 @@ import {
} from "./stack-param-lists"
import { NotificationSettingsScreen } from "@app/screens/settings-screen/notifications-screen"

const useStyles = makeStyles(({ colors }) => ({
bottomNavigatorStyle: {
height: "10%",
paddingTop: 4,
backgroundColor: colors.white,
borderTopColor: colors.grey4,
},
headerStyle: {
backgroundColor: colors.white,
},
title: {
color: colors.black,
},
}))

const RootNavigator = createStackNavigator<RootStackParamList>()

export const RootStack = () => {
Expand Down Expand Up @@ -571,3 +556,18 @@ export const PrimaryNavigator = () => {
</Tab.Navigator>
)
}

const useStyles = makeStyles(({ colors }) => ({
bottomNavigatorStyle: {
height: "10%",
paddingTop: 4,
backgroundColor: colors.white,
borderTopColor: colors.grey4,
},
headerStyle: {
backgroundColor: colors.white,
},
title: {
color: colors.black,
},
}))
43 changes: 29 additions & 14 deletions app/screens/receive-bitcoin-screen/receive-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useIsAuthed } from "@app/graphql/is-authed-context"
import { useI18nContext } from "@app/i18n/i18n-react"
import { requestNotificationPermission } from "@app/utils/notifications"
import { useIsFocused, useNavigation } from "@react-navigation/native"
import React, { useEffect } from "react"
import React, { useEffect, useState } from "react"
import { TouchableOpacity, View } from "react-native"
import { testProps } from "../../utils/testProps"
import { withMyLnUpdateSub } from "./my-ln-updates-sub"
Expand All @@ -18,6 +18,7 @@ import { NoteInput } from "@app/components/note-input"
import Icon from "react-native-vector-icons/Ionicons"
import { SetLightningAddressModal } from "@app/components/set-lightning-address-modal"
import { GaloyCurrencyBubble } from "@app/components/atomic/galoy-currency-bubble"
import { ModalNfc } from "@app/components/modal-nfc"

const ReceiveScreen = () => {
const {
Expand All @@ -32,6 +33,21 @@ const ReceiveScreen = () => {

const request = useReceiveBitcoin()

const [displayReceiveNfc, setDisplayReceiveNfc] = useState(false)

useEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity
style={styles.rotateIconHeaderRight}
onPress={() => setDisplayReceiveNfc(true)}
>
<Icon name="wifi" color={colors.black} size={25} />
</TouchableOpacity>
),
})
})

// notification permission
useEffect(() => {
let timeout: NodeJS.Timeout
Expand All @@ -45,19 +61,6 @@ const ReceiveScreen = () => {
return () => timeout && clearTimeout(timeout)
}, [isAuthed, isFocused])

useEffect(() => {
switch (request?.type) {
case Invoice.OnChain:
navigation.setOptions({ title: LL.ReceiveScreen.receiveViaOnchain() })
break
case Invoice.Lightning:
navigation.setOptions({ title: LL.ReceiveScreen.receiveViaInvoice() })
break
case Invoice.PayCode:
navigation.setOptions({ title: LL.ReceiveScreen.receiveViaPaycode() })
}
}, [request?.type, LL.ReceiveScreen, navigation])

useEffect(() => {
if (request?.state === PaymentRequestState.Paid) {
const id = setTimeout(() => navigation.goBack(), 5000)
Expand Down Expand Up @@ -246,6 +249,13 @@ const ReceiveScreen = () => {
isVisible={request.isSetLightningAddressModalVisible}
toggleModal={request.toggleIsSetLightningAddressModalVisible}
/>

<ModalNfc
isActive={displayReceiveNfc}
setIsActive={setDisplayReceiveNfc}
settlementAmount={request.settlementAmount}
receiveViaNFC={request.receiveViaNFC}
/>
</Screen>
</>
)
Expand Down Expand Up @@ -334,6 +344,11 @@ const useStyles = makeStyles(({ colors }) => ({
fontWeight: "700",
},
btcLow: {},
rotateIconHeaderRight: {
transform: [{ rotate: "90deg" }],
marginRight: 2,
padding: 8,
},
}))

export default withMyLnUpdateSub(ReceiveScreen)
54 changes: 54 additions & 0 deletions app/screens/receive-bitcoin-screen/use-receive-bitcoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ import { useLnUpdateHashPaid } from "@app/graphql/ln-update-context"
import { generateFutureLocalTime, secondsToH, secondsToHMS } from "./payment/helpers"
import { toastShow } from "@app/utils/toast"
import { useI18nContext } from "@app/i18n/i18n-react"
import fetch from "cross-fetch"

import crashlytics from "@react-native-firebase/crashlytics"
import { Alert, Share } from "react-native"
import { TranslationFunctions } from "@app/i18n/i18n-types"
import { BtcWalletDescriptor } from "@app/types/wallets"
import { ReceiveDestination } from "../send-bitcoin-screen/payment-destination/index.types"

gql`
query paymentRequest {
Expand Down Expand Up @@ -455,6 +457,57 @@ export const useReceiveBitcoin = () => {
readablePaymentRequest = `${pr.info.data.username}@${lnAddressHostname}`
}

const receiveViaNFC = async (
destination: ReceiveDestination,
settlementAmount: MoneyAmount<"BTC">,
) => {
const { callback, defaultDescription, k1 } = destination.validDestination
const { data } = await lnInvoiceCreate({
variables: {
input: {
walletId: prcd.receivingWalletDescriptor.id,
amount: settlementAmount.amount,
memo: prcd.memo || defaultDescription,
},
},
})

if (!data) {
Alert.alert(LL.RedeemBitcoinScreen.error())
return
}

const {
lnInvoiceCreate: { invoice, errors },
} = data

if ((errors && errors.length !== 0) || !invoice) {
console.error(errors, "error with lnInvoiceCreate")
Alert.alert(LL.RedeemBitcoinScreen.error())
return
}

const url = `${callback}${callback.includes("?") ? "&" : "?"}k1=${k1}&pr=${
invoice.paymentRequest
}`

const result = await fetch(url)

if (result.ok) {
const lnurlResponse = await result.json()
if (lnurlResponse?.status?.toLowerCase() !== "ok") {
console.error(lnurlResponse, "error with redeeming")
Alert.alert(LL.RedeemBitcoinScreen.redeemingError())
if (lnurlResponse?.reason) {
Alert.alert(lnurlResponse.reason)
}
}
} else {
console.error(result.text(), "error with submitting withdrawalRequest")
Alert.alert(LL.RedeemBitcoinScreen.submissionError())
}
}

return {
...prcd,
setType,
Expand All @@ -472,5 +525,6 @@ export const useReceiveBitcoin = () => {
isSetLightningAddressModalVisible,
toggleIsSetLightningAddressModalVisible,
readablePaymentRequest,
receiveViaNFC,
}
}
Loading

0 comments on commit f55ccf6

Please sign in to comment.