Skip to content

Setup react-native-wallet on Android #60270

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
87d8b01
Exclude wallet from autolinking in new dot standalone
zfurtak Mar 31, 2025
5970980
Adjust react-native-config
zfurtak Apr 1, 2025
42ab989
Make import conditional for ND
zfurtak Apr 1, 2025
ca76692
Merge branch 'main' into @zfurtak/introduce-wallet-to-expensify
zfurtak Apr 2, 2025
62202d4
Complete android add card to wallet flow
zfurtak Apr 4, 2025
c6225c6
Merge main
zfurtak Apr 4, 2025
e9b38c5
Adjust code
zfurtak Apr 4, 2025
83865b2
Remove console logs
zfurtak Apr 4, 2025
120b01a
Fix test
zfurtak Apr 7, 2025
0be0d26
Add new version of module
zfurtak Apr 7, 2025
7ab0bad
Fix
zfurtak Apr 7, 2025
e6f2c07
Fix
zfurtak Apr 7, 2025
8ca4728
Fix
zfurtak Apr 7, 2025
d669d49
Revert webpack changes
zfurtak Apr 7, 2025
785043e
Merge branch '@Skalakid/add-to-apple-wallet' into @zfurtak/introduce-…
zfurtak Apr 7, 2025
a1b9441
Checkout package lock
zfurtak Apr 7, 2025
fefa35c
Merge branch '@Skalakid/add-to-apple-wallet' into @zfurtak/introduce-…
zfurtak Apr 8, 2025
1614221
Merge branch '@Skalakid/add-to-apple-wallet' into @zfurtak/introduce-…
zfurtak Apr 8, 2025
862e7bd
Add missing functions to Wallet utils
Skalakid Apr 8, 2025
58c3e57
Update module
Skalakid Apr 8, 2025
ea68213
Merge remote-tracking branch 'origin' into @zfurtak/introduce-wallet-…
zfurtak Apr 8, 2025
81bd99c
Merge remote-tracking branch 'origin/@zfurtak/introduce-wallet-to-exp…
zfurtak Apr 8, 2025
ccd9c0b
Temporary solution for standalone android
zfurtak Apr 8, 2025
3a7ad33
Merge branch '@Skalakid/add-to-apple-wallet' into @zfurtak/introduce-…
zfurtak Apr 9, 2025
39288c5
Change file from ios to native
zfurtak Apr 14, 2025
af5c9cf
Merge branch 'main' into @zfurtak/introduce-wallet-to-expensify
zfurtak Apr 15, 2025
74ce3e4
Stop utolinking when sdk not available
zfurtak Apr 15, 2025
3c2a86c
Adjustments
zfurtak Apr 15, 2025
2d49a19
Fix checks
zfurtak Apr 15, 2025
d7740a8
Add card check
zfurtak Apr 15, 2025
c312a13
Fix eslint check
zfurtak Apr 15, 2025
03f3215
Adjustments
zfurtak Apr 16, 2025
c2cbd3e
Update library package
zfurtak Apr 16, 2025
50b1ac2
Adjust to review comments
zfurtak Apr 16, 2025
4543508
Remove blank line
zfurtak Apr 16, 2025
3a9b5c0
Adjust type imports
zfurtak Apr 16, 2025
b169ecd
Commit package-lock
zfurtak Apr 16, 2025
14a383b
Merge branch 'main' into @zfurtak/introduce-wallet-to-expensify
zfurtak Apr 16, 2025
0d500b0
Merge branch 'main' into @zfurtak/introduce-wallet-to-expensify
zfurtak Apr 23, 2025
1058708
Change requestWithSideEffects to API.write
zfurtak Apr 23, 2025
0a3feab
Adjust API call
zfurtak Apr 24, 2025
b243165
Adjust Mobile-Expensify version
zfurtak Apr 24, 2025
dd3b6fc
Adjustments:
zfurtak Apr 24, 2025
c13b0eb
Remove logs
zfurtak Apr 24, 2025
7477bfc
Fix multiple opening
zfurtak Apr 24, 2025
18a135b
Merge branch 'main' into @zfurtak/introduce-wallet-to-expensify
zfurtak Apr 24, 2025
0509fdc
Adjust api call
zfurtak Apr 25, 2025
0e66333
Adjust config
zfurtak Apr 25, 2025
7a9aa51
Adjustments
zfurtak Apr 25, 2025
cd826ac
Add comment
zfurtak Apr 25, 2025
50d7faf
Adjusted to review
zfurtak May 5, 2025
8912849
Update Mobile-Expensify version
zfurtak May 5, 2025
968f3dc
Add loading indicator
zfurtak May 6, 2025
2170dd6
Adjustments
zfurtak May 7, 2025
63463fb
Merge branch 'main' into @zfurtak/introduce-wallet-to-expensify
zfurtak May 7, 2025
dfc9dad
Adjustments
zfurtak May 7, 2025
9475767
Merge branch 'main' into @zfurtak/introduce-wallet-to-expensify
zfurtak May 8, 2025
302d3e6
Adjust label after adding card
zfurtak May 8, 2025
403f9d5
Adjust Mobile-Expensify version
zfurtak May 9, 2025
2944f32
Move checking wallet to button function
zfurtak May 12, 2025
29697ab
Make status check use last 4 digits
zfurtak May 13, 2025
cbbad79
Merge branch 'main' into @zfurtak/introduce-wallet-to-expensify
zfurtak May 13, 2025
52e0a88
Merge main and update MobileExpensify version
zfurtak May 13, 2025
217918a
Update Mobile-Expensify version
zfurtak May 13, 2025
734d295
Merge branch 'main' into @zfurtak/introduce-wallet-to-expensify
zfurtak May 14, 2025
ea77fc1
Revert to useEffect check
zfurtak May 14, 2025
72632f1
Repeat the test flowD
zfurtak May 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified modules/expensify-react-native-wallet.tgz
Binary file not shown.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 16 additions & 11 deletions react-native.config.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
module.exports = {
const isHybrid = process.env.IS_HYBRID_APP === 'true' ? true : false;

const config = {
assets: ['./assets/fonts/native'],
dependencies: {
// We need to unlink the react-native-wallet package from the android build
// because it's not supported yet and we want to prevent the build from failing
// due to missing Google TapAndPay SDK
'@expensify/react-native-wallet': {
platforms: {
android: null,
},
},
},
dependencies: {},
};

// We need to unlink the react-native-wallet package from the android standalone build
// to prevent the build from failing due to missing Google TapAndPay SDK
if (!isHybrid) {
config.dependencies['@expensify/react-native-wallet'] = {
platforms: {
android: null,
},
};
}

module.exports = config;
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import {AddToWalletButton as RNAddToWalletButton} from '@expensify/react-native-wallet';
import React, {useCallback, useEffect} from 'react';
import {View} from 'react-native';
import React, {useCallback, useEffect, useState} from 'react';
import {ActivityIndicator, View} from 'react-native';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import getPlatform from '@libs/getPlatform';
import {checkIfWalletIsAvailable, handleAddCardToWallet, isCardInWallet} from '@libs/Wallet/index';
import CONST from '@src/CONST';
import type AddToWalletButtonProps from './types';

function AddToWalletButton({card, cardHolderName, cardDescription, buttonStyle}: AddToWalletButtonProps) {
const [isWalletAvailable, setIsWalletAvailable] = React.useState<boolean>(false);
const [isInWallet, setIsInWallet] = React.useState<boolean | null>(null);
const {translate} = useLocalize();
const isCardAvailable = card.state === CONST.EXPENSIFY_CARD.STATE.OPEN;
const [isLoading, setIsLoading] = useState(false);
const theme = useTheme();
const platform = getPlatform() === CONST.PLATFORM.IOS ? 'Apple' : 'Google';
const styles = useThemeStyles();

const checkIfCardIsInWallet = useCallback(() => {
isCardInWallet(card)
Expand All @@ -18,35 +27,56 @@ function AddToWalletButton({card, cardHolderName, cardDescription, buttonStyle}:
})
.catch(() => {
setIsInWallet(false);
})
.finally(() => {
setIsLoading(false);
});
}, [card]);

const handleOnPress = useCallback(() => {
handleAddCardToWallet(card, cardHolderName, cardDescription, checkIfCardIsInWallet);
}, [card, cardDescription, cardHolderName, checkIfCardIsInWallet]);
setIsLoading(true);
handleAddCardToWallet(card, cardHolderName, cardDescription, () => setIsLoading(false));
}, [card, cardDescription, cardHolderName]);

useEffect(() => {
if (!isCardAvailable) {
return;
}

checkIfCardIsInWallet();
}, [checkIfCardIsInWallet]);
}, [checkIfCardIsInWallet, isCardAvailable, card]);

useEffect(() => {
if (!isCardAvailable) {
return;
}

checkIfWalletIsAvailable()
.then((result) => {
setIsWalletAvailable(result);
})
.catch(() => {
setIsWalletAvailable(false);
});
}, []);
}, [isCardAvailable]);

if (!isWalletAvailable || isInWallet == null) {
if (!isWalletAvailable || isInWallet == null || !isCardAvailable) {
return null;
}

if (isLoading) {
return (
<ActivityIndicator
size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE}
color={theme.spinner}
/>
);
}

if (isInWallet) {
return (
<View style={buttonStyle}>
<Text>{translate('cardPage.cardAlreadyInWallet')}</Text>;
<Text style={[styles.textLabelSupporting, styles.mt6]}>{translate('cardPage.cardAddedToWallet', {platform})}</Text>
</View>
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1732,7 +1732,7 @@ const translations = {
copyCardNumber: 'Copy card number',
updateAddress: 'Update address',
},
cardAlreadyInWallet: 'Card is already in wallet',
cardAddedToWallet: ({platform}: {platform: 'Google' | 'Apple'}) => `Added to ${platform} Wallet`,
cardDetailsLoadingFailure: 'An error occurred while loading the card details. Please check your internet connection and try again.',
validateCardTitle: "Let's make sure it's you",
enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Please enter the magic code sent to ${contactMethod} to view your card details. It should arrive within a minute or two.`,
Expand Down
2 changes: 1 addition & 1 deletion src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1730,7 +1730,7 @@ const translations = {
copyCardNumber: 'Copiar número de la tarjeta',
updateAddress: 'Actualizar dirección',
},
cardAlreadyInWallet: 'La tarjeta ya está en la billetera',
cardAddedToWallet: ({platform}: {platform: 'Google' | 'Apple'}) => `Añadida a ${platform} Wallet`,
cardDetailsLoadingFailure: 'Se ha producido un error al cargar los datos de la tarjeta. Comprueba tu conexión a Internet e inténtalo de nuevo.',
validateCardTitle: 'Asegurémonos de que eres tú',
enterMagicCode: ({contactMethod}: EnterMagicCodeParams) =>
Expand Down
10 changes: 0 additions & 10 deletions src/libs/API/parameters/CreateAppleDigitalWalletParams.ts

This file was deleted.

20 changes: 20 additions & 0 deletions src/libs/API/parameters/CreateDigitalWalletParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
type CreateDigitalAppleWalletParams = {
platform: 'ios';
appVersion: string;
// stringified {"certificates": string[]} object
certificates: string;
nonce: string;
nonceSignature: string;
};

type CreateDigitalGoogleWalletParams = {
platform: 'android';
appVersion: string;
walletAccountID: string;
deviceID: string;
};

type CreateDigitalWalletParams = CreateDigitalAppleWalletParams | CreateDigitalGoogleWalletParams;

export type {CreateDigitalAppleWalletParams, CreateDigitalGoogleWalletParams};
export default CreateDigitalWalletParams;
2 changes: 1 addition & 1 deletion src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,9 +389,9 @@ export type {ChangeTransactionsReportParams, TransactionThreadInfo} from './Chan
export type {default as ResetBankAccountSetupParams} from './ResetBankAccountSetupParams';
export type {default as SendRecapInAdminsRoomParams} from './SendRecapInAdminsRoomParams';
export type {default as SetPolicyProhibitedExpensesParams} from './SetPolicyProhibitedExpensesParams';
export type {default as CreateDigitalWalletParams} from './CreateDigitalWalletParams';
export type {default as GetGuideCallAvailabilityScheduleParams} from './GetGuideCallAvailabilitySchedule';
export type {default as GetEmphemeralTokenParams} from './GetEmphemeralTokenParams';
export type {default as CreateAppleDigitalWalletParams} from './CreateAppleDigitalWalletParams';
export type {default as CompleteConciergeCallParams} from './CompleteConciergeCallParams';
export type {default as FinishCorpayBankAccountOnboardingParams} from './FinishCorpayBankAccountOnboardingParams';
export type {default as ReopenReportParams} from './ReopenReportParams';
2 changes: 1 addition & 1 deletion src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1160,7 +1160,7 @@ type SideEffectRequestCommandParameters = {
[SIDE_EFFECT_REQUEST_COMMANDS.MERGE_INTO_ACCOUNT_AND_LOGIN]: Parameters.MergeIntoAccountAndLogInParams;
[SIDE_EFFECT_REQUEST_COMMANDS.LOG_OUT]: Parameters.LogOutParams;
[SIDE_EFFECT_REQUEST_COMMANDS.GET_EMPHEMERAL_TOKEN]: Parameters.GetEmphemeralTokenParams;
[SIDE_EFFECT_REQUEST_COMMANDS.CREATE_DIGITAL_WALLET]: Parameters.CreateAppleDigitalWalletParams;
[SIDE_EFFECT_REQUEST_COMMANDS.CREATE_DIGITAL_WALLET]: Parameters.CreateDigitalWalletParams;
};

type ApiRequestCommandParameters = WriteCommandParameters & ReadCommandParameters & SideEffectRequestCommandParameters;
Expand Down
53 changes: 53 additions & 0 deletions src/libs/Wallet/index.android.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {addCardToGoogleWallet, checkWalletAvailability, getCardStatusBySuffix, getSecureWalletInfo} from '@expensify/react-native-wallet';
import type {AndroidCardData, AndroidWalletData, CardStatus, TokenizationStatus} from '@expensify/react-native-wallet';
import {Alert} from 'react-native';
import {openWalletPage} from '@libs/actions/PaymentMethods';
import {createDigitalGoogleWallet} from '@libs/actions/Wallet';
import Log from '@libs/Log';
import type {Card} from '@src/types/onyx';

function checkIfWalletIsAvailable(): Promise<boolean> {
return checkWalletAvailability();
}

function handleAddCardToWallet(card: Card, cardHolderName: string, cardDescription: string, onFinished?: () => void) {
getSecureWalletInfo()
.then((walletData: AndroidWalletData) => {
createDigitalGoogleWallet({cardHolderName, ...walletData})
.then((cardData: AndroidCardData) => {
addCardToGoogleWallet(cardData)
.then((status: TokenizationStatus) => {
if (status === 'success') {
Log.info('Card added to wallet');
openWalletPage();
} else {
onFinished?.();
}
})
.catch((error) => {
Log.warn(`addCardToGoogleWallet error: ${error}`);
Alert.alert('Failed to add card to wallet.', 'Please try again later.');
});
})

.catch((error) => Log.warn(`createDigitalWallet error: ${error}`));
})
.catch((error) => Log.warn(`getSecureWalletInfo error: ${error}`));
}

function isCardInWallet(card: Card): Promise<boolean> {
if (!card.lastFourPAN) {
return Promise.resolve(false);
}
return getCardStatusBySuffix(card.lastFourPAN)
.then((status: CardStatus) => {
Log.info(`Card status: ${status}`);
return status === 'active';
})
.catch((error) => {
Log.warn(`getCardTokenStatus error: ${error}`);
return false;
});
}

export {handleAddCardToWallet, isCardInWallet, checkIfWalletIsAvailable};
8 changes: 3 additions & 5 deletions src/libs/Wallet/index.ios.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import {addCardToAppleWallet, checkWalletAvailability, getCardStatusByIdentifier, getCardStatusBySuffix} from '@expensify/react-native-wallet';
import type {IOSCardData} from '@expensify/react-native-wallet/lib/typescript/src/NativeWallet';
import type {IOSCardData} from '@expensify/react-native-wallet';
import {Alert} from 'react-native';
import {issuerEncryptPayloadCallback} from '@libs/actions/Wallet';
import Log from '@libs/Log';
import CONST from '@src/CONST';
import type {Card} from '@src/types/onyx';

const ExpensifyCardNetwork = CONST.COMPANY_CARDS.CARD_TYPE.VISA.toUpperCase();

function checkIfWalletIsAvailable(): Promise<boolean> {
return checkWalletAvailability();
}

function handleAddCardToWallet(card: Card, cardHolderName: string, cardDescription: string, onFinished?: () => void) {
const data = {
network: ExpensifyCardNetwork,
network: CONST.COMPANY_CARDS.CARD_TYPE.VISA,
lastDigits: card.lastFourPAN,
cardDescription,
cardHolderName,
Expand All @@ -38,7 +36,7 @@ function isCardInWallet(card: Card): Promise<boolean> {

let callback = null;
if (card.token) {
callback = getCardStatusByIdentifier(card.token, ExpensifyCardNetwork);
callback = getCardStatusByIdentifier(card.token, CONST.COMPANY_CARDS.CARD_TYPE.VISA);
} else if (card.lastFourPAN) {
callback = getCardStatusBySuffix(card.lastFourPAN);
}
Expand Down
48 changes: 44 additions & 4 deletions src/libs/actions/Wallet.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {IOSEncryptPayload} from '@expensify/react-native-wallet/lib/typescript/src/NativeWallet';
import type {AndroidCardData, IOSEncryptPayload} from '@expensify/react-native-wallet';
import type {OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
Expand All @@ -8,7 +8,7 @@ import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs
import Log from '@libs/Log';
import type CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {WalletAdditionalQuestionDetails} from '@src/types/onyx';
import type {ProvisioningCardData, WalletAdditionalQuestionDetails} from '@src/types/onyx';
import pkg from '../../../package.json';

type WalletQuestionAnswer = {
Expand Down Expand Up @@ -267,12 +267,51 @@ function issuerEncryptPayloadCallback(nonce: string, nonceSignature: string, cer
ephemeralPublicKey: data.ephemeralPublicKey,
} as IOSEncryptPayload;
})
.catch((e) => {
Log.warn(`issuerEncryptPayloadCallback error: ${e}`);
.catch((error) => {
Log.warn(`issuerEncryptPayloadCallback error: ${error}`);
return {} as IOSEncryptPayload;
});
}

/**
* Add card to digital wallet
*
* @param walletAcountID ID of the wallet on user's phone
* @param deviceID ID of user's phone
*/
function createDigitalGoogleWallet({walletAccountID, deviceID, cardHolderName}: {deviceID: string; walletAccountID: string; cardHolderName: string}): Promise<AndroidCardData> {
// eslint-disable-next-line rulesdir/no-api-side-effects-method
return API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.CREATE_DIGITAL_WALLET, {
platform: 'android',
appVersion: pkg.version,
walletAccountID,
deviceID,
})
.then((response) => {
const data = response as unknown as ProvisioningCardData;
return {
network: data.network,
opaquePaymentCard: data.opaquePaymentCard,
cardHolderName,
lastDigits: data.lastDigits,
userAddress: {
name: data.userAddress.name,
addressOne: data.userAddress.address1,
addressTwo: data.userAddress.address2,
administrativeArea: data.userAddress.state,
locality: data.userAddress.city,
countryCode: data.userAddress.country,
postalCode: data.userAddress.postal_code,
phoneNumber: data.userAddress.phone,
},
} as AndroidCardData;
})
.catch((error) => {
Log.warn(`createDigitalGoogleWallet error: ${error}`);
return {} as AndroidCardData;
});
}

export {
openOnfidoFlow,
openInitialSettingsPage,
Expand All @@ -286,4 +325,5 @@ export {
setKYCWallSource,
resetWalletAdditionalDetailsDraft,
issuerEncryptPayloadCallback,
createDigitalGoogleWallet,
};
Loading