Skip to content

Commit

Permalink
fix(sends): Enforce token override when sending specific token via QR…
Browse files Browse the repository at this point in the history
… code (#6250)

### Description

For
[ACT-1462](https://linear.app/valora/issue/ACT-1462/send-flow-qr-scanner-not-preserving-token-id-override).
Updates the send flow + QR path to pass through a token override when
entering via a token details screen.

### Test plan

Unit tests updated + manual tests. See video below.



https://github.com/user-attachments/assets/ee7a3aea-93de-4ab9-bb91-cd6fed629d61


### Related issues

- Fixes
[ACT-1462](https://linear.app/valora/issue/ACT-1462/send-flow-qr-scanner-not-preserving-token-id-override).

### Backwards compatibility

<!-- Brief explanation of why these changes are/are not backwards
compatible. -->

### Network scalability

If a new NetworkId and/or Network are added in the future, the changes
in this PR will:

- [x] Continue to work without code changes, OR trigger a compilation
error (guaranteeing we find it when a new network is added)
  • Loading branch information
jophish authored Nov 25, 2024
1 parent 17eaf65 commit 3516a13
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 42 deletions.
8 changes: 7 additions & 1 deletion src/navigator/QRNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,13 @@ type ScannerSceneProps = NativeStackScreenProps<QRTabParamList, Screens.QRScanne
function ScannerScene({ route }: ScannerSceneProps) {
const lastScannedQR = useRef('')
const dispatch = useDispatch()
const defaultOnQRCodeDetected = (qrCode: QrCode) => dispatch(handleQRCodeDetected(qrCode))
const defaultOnQRCodeDetected = (qrCode: QrCode) =>
dispatch(
handleQRCodeDetected({
qrCode,
defaultTokenIdOverride: route?.params?.defaultTokenIdOverride,
})
)
const { onQRCodeDetected: onQRCodeDetectedParam = defaultOnQRCodeDetected } = route.params || {}
const isFocused = useIsFocused()
const [wasFocused, setWasFocused] = useState(isFocused)
Expand Down
1 change: 1 addition & 0 deletions src/navigator/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ export type QRTabParamList = {
| {
showSecureSendStyling?: true
onQRCodeDetected?: (qrCode: QrCode) => void
defaultTokenIdOverride?: string
}
| undefined
}
18 changes: 11 additions & 7 deletions src/qrcode/utils.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe('handleQRCodeDefault', () => {
const link = `${DEEP_LINK_URL_SCHEME}://wallet/hooks/enablePreview?hooksApiUrl=https://192.168.0.42:18000`
const qrCode: QrCode = { type: QRCodeTypes.QR_CODE, data: link }

await expectSaga(handleQRCodeDefault, handleQRCodeDetected(qrCode))
await expectSaga(handleQRCodeDefault, handleQRCodeDetected({ qrCode }))
.provide([[select(allowHooksPreviewSelector), true]])
.run()

Expand All @@ -92,7 +92,7 @@ describe('handleQRCodeDefault', () => {
it('navigates to the send amount screen with a valid QR code', async () => {
const qrCode: QrCode = { type: QRCodeTypes.QR_CODE, data: urlFromUriData(mockQrCodeData) }

await expectSaga(handleQRCodeDefault, handleQRCodeDetected(qrCode))
await expectSaga(handleQRCodeDefault, handleQRCodeDetected({ qrCode }))
.withState(createMockStore({}).getState())
.provide([[select(recipientInfoSelector), mockRecipientInfo]])
.run()
Expand All @@ -117,7 +117,7 @@ describe('handleQRCodeDefault', () => {
async (data) => {
const qrCode: QrCode = { type: QRCodeTypes.QR_CODE, data }

await expectSaga(handleQRCodeDefault, handleQRCodeDetected(qrCode))
await expectSaga(handleQRCodeDefault, handleQRCodeDetected({ qrCode }))
.withState(createMockStore({}).getState())
.provide([[select(recipientInfoSelector), mockRecipientInfo]])
.run()
Expand All @@ -140,12 +140,12 @@ describe('handleQRCodeDefault', () => {
it('throws an error when the QR code data is invalid', async () => {
const qrCode: QrCode = { type: QRCodeTypes.QR_CODE, data: mockAccount.replace('0x', '') }

await expectSaga(handleQRCodeDefault, handleQRCodeDetected(qrCode))
await expectSaga(handleQRCodeDefault, handleQRCodeDetected({ qrCode }))
.withState(createMockStore({}).getState())
.put(showError(ErrorMessages.QR_FAILED_INVALID_ADDRESS))
.run()
})
it('navigates to the send amount screen with a qr code with an empty display name', async () => {
it('navigates to the send amount screen with a qr code with an empty display name and token override', async () => {
const qrCode: QrCode = {
type: QRCodeTypes.QR_CODE,
data: urlFromUriData({
Expand All @@ -154,7 +154,10 @@ describe('handleQRCodeDefault', () => {
}),
}

await expectSaga(handleQRCodeDefault, handleQRCodeDetected(qrCode))
await expectSaga(
handleQRCodeDefault,
handleQRCodeDetected({ qrCode, defaultTokenIdOverride: 'some-token-id' })
)
.withState(createMockStore({}).getState())
.provide([
[select(e164NumberToAddressSelector), {}],
Expand All @@ -171,6 +174,7 @@ describe('handleQRCodeDefault', () => {
thumbnailPath: undefined,
recipientType: RecipientType.Address,
},
defaultTokenIdOverride: 'some-token-id',
forceTokenId: false,
})
expect(AppAnalytics.track).toHaveBeenCalledWith(QrScreenEvents.qr_scanned, qrCode)
Expand All @@ -184,7 +188,7 @@ describe('handleQRCodeDefault', () => {
}),
}

await expectSaga(handleQRCodeDefault, handleQRCodeDetected(qrCode))
await expectSaga(handleQRCodeDefault, handleQRCodeDetected({ qrCode }))
.withState(createMockStore({}).getState())
.provide([
[select(e164NumberToAddressSelector), {}],
Expand Down
12 changes: 10 additions & 2 deletions src/qrcode/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,10 @@ function* extractQRAddressData(qrCode: QrCode) {

// Catch all handler for QR Codes
// includes support for WalletConnect, hooks, and send flow (non-secure send)
export function* handleQRCodeDefault({ qrCode }: HandleQRCodeDetectedAction) {
export function* handleQRCodeDefault({
qrCode,
defaultTokenIdOverride,
}: HandleQRCodeDetectedAction) {
AppAnalytics.track(QrScreenEvents.qr_scanned, qrCode)

const walletConnectEnabled: boolean = yield* call(isWalletConnectEnabled, qrCode.data)
Expand All @@ -173,7 +176,12 @@ export function* handleQRCodeDefault({ qrCode }: HandleQRCodeDetectedAction) {
const recipientInfo: RecipientInfo = yield* select(recipientInfoSelector)
const cachedRecipient = getRecipientFromAddress(qrData.address, recipientInfo)

yield* call(handleSendPaymentData, qrData, true, cachedRecipient)
yield* call(handleSendPaymentData, {
data: qrData,
isFromScan: true,
cachedRecipient,
defaultTokenIdOverride,
})
}

export function* handleQRCodeSecureSend({
Expand Down
21 changes: 19 additions & 2 deletions src/send/SelectRecipientButtons.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ import { createMockStore } from 'test/utils'

jest.mock('src/statsig')

const renderComponent = (phoneNumberVerified = false) => {
function renderComponent(phoneNumberVerified: boolean = false, defaultTokenIdOverride?: string) {
const onPermissionsGranted = jest.fn()
const tree = render(
<Provider store={createMockStore({ app: { phoneNumberVerified } })}>
<SelectRecipientButtons onContactsPermissionGranted={onPermissionsGranted} />
<SelectRecipientButtons
onContactsPermissionGranted={onPermissionsGranted}
defaultTokenIdOverride={defaultTokenIdOverride}
/>
</Provider>
)
return { ...tree, onPermissionsGranted }
Expand Down Expand Up @@ -76,6 +79,20 @@ describe('SelectRecipientButtons', () => {
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_scan_qr)
expect(navigate).toHaveBeenCalledWith(Screens.QRNavigator, {
screen: Screens.QRScanner,
params: {
defaultTokenIdOverride: undefined,
},
})
})
it('navigates to QR screen with an override when QR button is pressed', async () => {
const { findByTestId } = renderComponent(false, 'some-token-id')
fireEvent.press(await findByTestId('SelectRecipient/QR'))
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_scan_qr)
expect(navigate).toHaveBeenCalledWith(Screens.QRNavigator, {
screen: Screens.QRScanner,
params: {
defaultTokenIdOverride: 'some-token-id',
},
})
})
it('invokes permissions granted callback when contacts button is pressed with phone verified and contacts permission granted', async () => {
Expand Down
8 changes: 6 additions & 2 deletions src/send/SelectRecipientButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,13 @@ import { navigateToPhoneSettings } from 'src/utils/linking'

type Props = {
onContactsPermissionGranted: () => void
defaultTokenIdOverride?: string
}

export default function SelectRecipientButtons({ onContactsPermissionGranted }: Props) {
export default function SelectRecipientButtons({
onContactsPermissionGranted,
defaultTokenIdOverride,
}: Props) {
const { t } = useTranslation()

const phoneNumberVerified = useSelector(phoneNumberVerifiedSelector)
Expand Down Expand Up @@ -112,7 +116,7 @@ export default function SelectRecipientButtons({ onContactsPermissionGranted }:

const onPressQR = () => {
AppAnalytics.track(SendEvents.send_select_recipient_scan_qr)
navigate(Screens.QRNavigator, { screen: Screens.QRScanner })
navigate(Screens.QRNavigator, { screen: Screens.QRScanner, params: { defaultTokenIdOverride } })
}

const onPressConnectPhoneNumber = () => {
Expand Down
21 changes: 21 additions & 0 deletions src/send/SendSelectRecipient.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,29 @@ describe('SendSelectRecipient', () => {
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_scan_qr)
expect(navigate).toHaveBeenCalledWith(Screens.QRNavigator, {
screen: Screens.QRScanner,
params: {
defaultTokenIdOverride: undefined,
},
})
})
it('navigates to QR screen with an override when QR button is pressed', async () => {
const store = createMockStore(defaultStore)

const { getByTestId } = render(
<Provider store={store}>
<SendSelectRecipient {...mockScreenProps({ defaultTokenIdOverride: 'some-token-id' })} />
</Provider>
)
fireEvent.press(getByTestId('SelectRecipient/QR'))
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_scan_qr)
expect(navigate).toHaveBeenCalledWith(Screens.QRNavigator, {
screen: Screens.QRScanner,
params: {
defaultTokenIdOverride: 'some-token-id',
},
})
})

it('shows QR, sync contacts and get started section when no prior recipients', async () => {
const store = createMockStore({})

Expand Down
5 changes: 4 additions & 1 deletion src/send/SendSelectRecipient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,10 @@ function SendSelectRecipient({ route }: Props) {
) : (
<>
{inviteRewardsActive && <InviteRewardsCard />}
<SelectRecipientButtons onContactsPermissionGranted={onContactsPermissionGranted} />
<SelectRecipientButtons
defaultTokenIdOverride={defaultTokenIdOverride}
onContactsPermissionGranted={onContactsPermissionGranted}
/>
{activeView === SelectRecipientView.Recent && recentRecipients.length ? (
<RecipientPicker
testID={'SelectRecipient/RecentRecipientPicker'}
Expand Down
10 changes: 9 additions & 1 deletion src/send/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export enum Actions {
export interface HandleQRCodeDetectedAction {
type: Actions.BARCODE_DETECTED
qrCode: QrCode
defaultTokenIdOverride?: string
}

export interface HandleQRCodeDetectedSecureSendAction {
Expand Down Expand Up @@ -70,9 +71,16 @@ export type ActionTypes =
| SendPaymentFailureAction
| UpdateLastUsedCurrencyAction

export const handleQRCodeDetected = (qrCode: QrCode): HandleQRCodeDetectedAction => ({
export const handleQRCodeDetected = ({
qrCode,
defaultTokenIdOverride,
}: {
qrCode: QrCode
defaultTokenIdOverride?: string
}): HandleQRCodeDetectedAction => ({
type: Actions.BARCODE_DETECTED,
qrCode,
defaultTokenIdOverride,
})

export const handleQRCodeDetectedSecureSend = (
Expand Down
Loading

0 comments on commit 3516a13

Please sign in to comment.