diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 18db2efe2193..756af51050a5 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -398,6 +398,7 @@ PODS: - React-jsiexecutor - React-jsinspector - React-perflogger + - React-rendererconsistency - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) @@ -415,6 +416,7 @@ PODS: - React-jsiexecutor - React-jsinspector - React-perflogger + - React-rendererconsistency - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) @@ -431,6 +433,7 @@ PODS: - React-jsiexecutor - React-jsinspector - React-perflogger + - React-rendererconsistency - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) @@ -449,6 +452,7 @@ PODS: - React-jsiexecutor - React-jsinspector - React-perflogger + - React-rendererconsistency - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) @@ -466,6 +470,7 @@ PODS: - React-jsiexecutor - React-jsinspector - React-perflogger + - React-rendererconsistency - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) @@ -483,6 +488,7 @@ PODS: - React-jsiexecutor - React-jsinspector - React-perflogger + - React-rendererconsistency - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) @@ -500,6 +506,7 @@ PODS: - React-jsiexecutor - React-jsinspector - React-perflogger + - React-rendererconsistency - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) @@ -517,6 +524,7 @@ PODS: - React-jsiexecutor - React-jsinspector - React-perflogger + - React-rendererconsistency - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) @@ -534,6 +542,7 @@ PODS: - React-jsiexecutor - React-jsinspector - React-perflogger + - React-rendererconsistency - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) @@ -551,6 +560,7 @@ PODS: - React-jsiexecutor - React-jsinspector - React-perflogger + - React-rendererconsistency - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) @@ -568,6 +578,7 @@ PODS: - React-jsiexecutor - React-jsinspector - React-perflogger + - React-rendererconsistency - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) @@ -585,6 +596,7 @@ PODS: - React-jsiexecutor - React-jsinspector - React-perflogger + - React-rendererconsistency - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) @@ -602,6 +614,7 @@ PODS: - React-jsiexecutor - React-jsinspector - React-perflogger + - React-rendererconsistency - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) @@ -619,6 +632,7 @@ PODS: - React-jsiexecutor - React-jsinspector - React-perflogger + - React-rendererconsistency - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) @@ -693,6 +707,7 @@ PODS: - React-Fabric/componentregistry (= 0.77.1) - React-Fabric/componentregistrynative (= 0.77.1) - React-Fabric/components (= 0.77.1) + - React-Fabric/consistency (= 0.77.1) - React-Fabric/core (= 0.77.1) - React-Fabric/dom (= 0.77.1) - React-Fabric/imagemanager (= 0.77.1) @@ -884,6 +899,27 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga + - React-Fabric/consistency (0.77.1): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core - React-Fabric/core (0.77.1): - DoubleConversion - fast_float (= 6.1.4) @@ -2042,6 +2078,27 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - react-native-wallet (0.1.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-webrtc (124.0.5): - JitsiWebRTC (~> 124.0.0) - React-Core @@ -2949,6 +3006,7 @@ DEPENDENCIES: - react-native-release-profiler (from `../node_modules/react-native-release-profiler`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-view-shot (from `../node_modules/react-native-view-shot`) + - "react-native-wallet (from `../node_modules/@expensify/react-native-wallet`)" - react-native-webrtc (from `../node_modules/react-native-webrtc`) - react-native-webview (from `../node_modules/react-native-webview`) - React-nativeconfig (from `../node_modules/react-native/ReactCommon`) @@ -3198,6 +3256,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-safe-area-context" react-native-view-shot: :path: "../node_modules/react-native-view-shot" + react-native-wallet: + :path: "../node_modules/@expensify/react-native-wallet" react-native-webrtc: :path: "../node_modules/react-native-webrtc" react-native-webview: @@ -3321,7 +3381,7 @@ SPEC CHECKSUMS: AirshipServiceExtension: 9c73369f426396d9fb9ff222d86d842fac76ba46 AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa AppLogs: 3bc4e9b141dbf265b9464409caaa40416a9ee0e0 - boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 + boost: 659a89341ea4ab3df8259733813b52f26d8be9a5 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb EXAV: 57ea461614a714b8b8fcc72c6fbc298b8f1ec78b EXImageLoader: 759063a65ab016b836f73972d3bb25404888713d @@ -3380,13 +3440,13 @@ SPEC CHECKSUMS: RCTTypeSafety: 031cefa254a1df313a196f105b8fcffdab1c5ab6 React: 8edfc46c315852ec88ea4a29d5e79019af3dc667 React-callinvoker: 4450b01574dfc7a8f074f7e29e6965ac04859c8f - React-Core: 13bdd0a0125fdaecdee747b62e02d2a65b026520 + React-Core: 0ac3acde025b10da5ccb9970547ab60a863654ce React-CoreModules: ebe93fa403bbd4d0909de105ffd34eeaad355083 React-cxxreact: af8a2be3edfed0e1168279eea443f95a7285602e React-debug: b0f7271aeacc2eb9e34f863397dcfc204ef721c0 React-defaultsnativemodule: 2ad21fff895dabfb7db60ee1c37d0a3866229430 React-domnativemodule: f788e7169988a61ac38a5fc493ac02b5fbda7d6b - React-Fabric: 8b404451db2bb07bcf0e3f6dfebfb26455e4d7ce + React-Fabric: 012ec12475cf941575871fdc006cf934090a9840 React-FabricComponents: 40cecebced128b2aa50c25a5fa6fdcd3b175286d React-FabricImage: 81787fa643b67f1327b4aa5fa1678dc73a771e34 React-featureflags: 23d3dcdac6c9badeeb631db8a0883c7a3108d580 @@ -3424,11 +3484,12 @@ SPEC CHECKSUMS: react-native-release-profiler: dffea1dd7929bf5cf7da5ad4697dea7d9a062377 react-native-safe-area-context: b3edb1da341e5e61f865763d9e0b6d3d34706464 react-native-view-shot: bb169342812ded991a4a0387e7d0b17cb515e62a + react-native-wallet: 46df147c6089b2490dc5f4cbf25c58ce70b0215d react-native-webrtc: 0f1c94069ff1eb9d8fb1618c2dc71f73542c8cfa react-native-webview: b375842af66a9f0ab979378bdc8b26eeb5d8e3ee React-nativeconfig: cb207ebba7cafce30657c7ad9f1587a8f32e4564 React-NativeModulesApple: 82a8bee52df9f5b378195a500f22be3a6ef0f890 - React-perflogger: 8152bab3f0eb4b8751f282f9af7caed2c823a9ea + React-perflogger: a8a27aa65a47f740eff65bb38b79baacb38a50d5 React-performancetimeline: 3ef4a640b56f9c7ec5f52bd93217b9b607c37cf4 React-RCTActionSheet: 0fdf55fb8724856d63ca8c63cdb4e2325e15e8ec React-RCTAnimation: b93f5a1675cc2599e96851fec13c909fdfb1d6bb @@ -3442,7 +3503,7 @@ SPEC CHECKSUMS: React-RCTSettings: 433c9f6a070bcecbe5a44d5009326b4d6f3b0667 React-RCTText: 46249950f8d8738b90a60883d19b5bef09f0a296 React-RCTVibration: 8f41e85ab6d40c7db6111ca9e8c7492c8de374fb - React-rendererconsistency: c766ce7261ab6ed6be7bc155c403e29436d4f156 + React-rendererconsistency: d59654ad59217708509d1bb6373358be321613a4 React-rendererdebug: 1f619b295f346242842f3accee23e8394b995d3c React-rncore: cafe45e14d870bbecbbf4bd89e12ef3b596e1f2d React-RuntimeApple: 7f27fb75a37e00a8e1efaa6e8f7a5b653871fb1b diff --git a/modules/expensify-react-native-wallet.tgz b/modules/expensify-react-native-wallet.tgz new file mode 100644 index 000000000000..cbe97d29c014 Binary files /dev/null and b/modules/expensify-react-native-wallet.tgz differ diff --git a/package-lock.json b/package-lock.json index e1e45102c0fe..ce28aa7ba538 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-hybrid-app": "file:./modules/hybrid-app", "@expensify/react-native-live-markdown": "0.1.244", + "@expensify/react-native-wallet": "file:modules/expensify-react-native-wallet.tgz", "@expo/metro-runtime": "^4.0.1", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -3768,6 +3769,23 @@ "react-native-reanimated": ">=3.17.0" } }, + "node_modules/@expensify/react-native-wallet": { + "version": "0.1.0", + "resolved": "file:modules/expensify-react-native-wallet.tgz", + "integrity": "sha512-yqwvXJO4F0Lb5lXKYTbLlQKciiDyG4qk14p92VmDwuuumWTHagbHJYGXbyh3L8fLa2ZJHLz0SxLcC6+gI0AXDA==", + "license": "MIT", + "workspaces": [ + "./example" + ], + "dependencies": { + "expo": "^52.0.31", + "expo-image": "^2.0.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@expo/bunyan": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@expo/bunyan/-/bunyan-4.0.1.tgz", diff --git a/package.json b/package.json index aa240fed4093..b5aa064c7de6 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-hybrid-app": "file:./modules/hybrid-app", "@expensify/react-native-live-markdown": "0.1.244", + "@expensify/react-native-wallet": "file:modules/expensify-react-native-wallet.tgz", "@expo/metro-runtime": "^4.0.1", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", diff --git a/react-native.config.js b/react-native.config.js index 773375378acd..ce16368ab2e9 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -7,4 +7,14 @@ module.exports = { android: {sourceDir: androidSourceDir}, }, 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, + }, + }, + }, }; diff --git a/src/CONST.ts b/src/CONST.ts index 66b354d24f69..2fd7c0d2cd56 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -775,6 +775,7 @@ const CONST = { HELP_SIDE_PANEL: 'newDotHelpSidePanel', RECEIPT_LINE_ITEMS: 'receiptLineItems', LEFT_HAND_BAR: 'leftHandBar', + WALLET: 'newdotWallet', }, BUTTON_STATES: { DEFAULT: 'default', diff --git a/src/components/AddToWalletButton/index.ios.tsx b/src/components/AddToWalletButton/index.ios.tsx new file mode 100644 index 000000000000..ee8720213a22 --- /dev/null +++ b/src/components/AddToWalletButton/index.ios.tsx @@ -0,0 +1,65 @@ +import {AddToWalletButton as RNAddToWalletButton} from '@expensify/react-native-wallet'; +import React, {useCallback, useEffect} from 'react'; +import {View} from 'react-native'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import {checkIfWalletIsAvailable, handleAddCardToWallet, isCardInWallet} from '@libs/Wallet/index'; +import type AddToWalletButtonProps from './types'; + +function AddToWalletButton({card, cardHolderName, cardDescription, buttonStyle}: AddToWalletButtonProps) { + const [isWalletAvailable, setIsWalletAvailable] = React.useState(false); + const [isInWallet, setIsInWallet] = React.useState(null); + const {translate} = useLocalize(); + + const checkIfCardIsInWallet = useCallback(() => { + isCardInWallet(card) + .then((result) => { + setIsInWallet(result); + }) + .catch(() => { + setIsInWallet(false); + }); + }, [card]); + + const handleOnPress = useCallback(() => { + handleAddCardToWallet(card, cardHolderName, cardDescription, checkIfCardIsInWallet); + }, [card, cardDescription, cardHolderName, checkIfCardIsInWallet]); + + useEffect(() => { + checkIfCardIsInWallet(); + }, [checkIfCardIsInWallet]); + + useEffect(() => { + checkIfWalletIsAvailable() + .then((result) => { + setIsWalletAvailable(result); + }) + .catch(() => { + setIsWalletAvailable(false); + }); + }, []); + + if (!isWalletAvailable || isInWallet == null) { + return null; + } + + if (isInWallet) { + return ( + + {translate('cardPage.cardAlreadyInWallet')}; + + ); + } + + return ( + + ); +} + +AddToWalletButton.displayName = 'AddToWalletButton'; + +export default AddToWalletButton; diff --git a/src/components/AddToWalletButton/index.tsx b/src/components/AddToWalletButton/index.tsx new file mode 100644 index 000000000000..e4c1ca3b5e69 --- /dev/null +++ b/src/components/AddToWalletButton/index.tsx @@ -0,0 +1,10 @@ +import type AddToWalletButtonProps from './types'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function AddToWalletButton(_props: AddToWalletButtonProps) { + return null; +} + +AddToWalletButton.displayName = 'AddToWalletButton'; + +export default AddToWalletButton; diff --git a/src/components/AddToWalletButton/types.ts b/src/components/AddToWalletButton/types.ts new file mode 100644 index 000000000000..32bebc211f13 --- /dev/null +++ b/src/components/AddToWalletButton/types.ts @@ -0,0 +1,11 @@ +import type {ViewStyle} from 'react-native'; +import type {Card} from '@src/types/onyx'; + +type AddToWalletButtonProps = { + card: Card; + cardHolderName: string; + cardDescription: string; + buttonStyle?: ViewStyle; +}; + +export default AddToWalletButtonProps; diff --git a/src/languages/en.ts b/src/languages/en.ts index 66f9fc933f74..b79188472c7b 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1684,6 +1684,7 @@ const translations = { copyCardNumber: 'Copy card number', updateAddress: 'Update address', }, + cardAlreadyInWallet: 'Card is already in 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.`, diff --git a/src/languages/es.ts b/src/languages/es.ts index a6388799c309..0632d2080790 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1683,6 +1683,7 @@ const translations = { copyCardNumber: 'Copiar número de la tarjeta', updateAddress: 'Actualizar dirección', }, + cardAlreadyInWallet: 'La tarjeta ya está en la billetera', 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) => diff --git a/src/libs/API/parameters/CreateAppleDigitalWalletParams.ts b/src/libs/API/parameters/CreateAppleDigitalWalletParams.ts new file mode 100644 index 000000000000..702687c23c17 --- /dev/null +++ b/src/libs/API/parameters/CreateAppleDigitalWalletParams.ts @@ -0,0 +1,10 @@ +type CreateAppleSigitalWalletParams = { + platform: string; + appVersion: string; + // stringified {"certificates": string[]} + certificates: string; + nonce: string; + nonceSignature: string; +}; + +export default CreateAppleSigitalWalletParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index d46b4cb742fa..173098143694 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -382,3 +382,4 @@ export type {default as ChangeReportPolicyParams} from './ChangeReportPolicyPara 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 CreateAppleDigitalWalletParams} from './CreateAppleDigitalWalletParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 4f771a8d118f..bfe7901cbb40 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -1100,6 +1100,7 @@ const SIDE_EFFECT_REQUEST_COMMANDS = { // PayMoneyRequestOnSearch only works online (pattern C) and we need to play the success sound only when the request is successful PAY_MONEY_REQUEST_ON_SEARCH: 'PayMoneyRequestOnSearch', LOG_OUT: 'LogOut', + CREATE_DIGITAL_WALLET: 'CreateDigitalWallet', } as const; type SideEffectRequestCommand = ValueOf; @@ -1119,6 +1120,7 @@ type SideEffectRequestCommandParameters = { [SIDE_EFFECT_REQUEST_COMMANDS.BANK_ACCOUNT_CREATE_CORPAY]: Parameters.BankAccountCreateCorpayParams; [SIDE_EFFECT_REQUEST_COMMANDS.PAY_MONEY_REQUEST_ON_SEARCH]: Parameters.PayMoneyRequestOnSearchParams; [SIDE_EFFECT_REQUEST_COMMANDS.LOG_OUT]: Parameters.LogOutParams; + [SIDE_EFFECT_REQUEST_COMMANDS.CREATE_DIGITAL_WALLET]: Parameters.CreateAppleDigitalWalletParams; [SIDE_EFFECT_REQUEST_COMMANDS.GET_EMPHEMERAL_TOKEN]: EmptyObject; }; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index f412cce51612..ec0f92d1cddb 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -70,6 +70,10 @@ function canUseLeftHandBar(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.LEFT_HAND_BAR) || canUseAllBetas(betas); } +function canUseInAppProvisioning(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.WALLET) || canUseAllBetas(betas); +} + export default { canUseDefaultRooms, canUseLinkPreviews, @@ -86,4 +90,5 @@ export default { canUseTalkToAISales, canUseProhibitedExpenses, canUseLeftHandBar, + canUseInAppProvisioning, }; diff --git a/src/libs/Wallet/index.ios.ts b/src/libs/Wallet/index.ios.ts new file mode 100644 index 000000000000..d1a26ff9f92d --- /dev/null +++ b/src/libs/Wallet/index.ios.ts @@ -0,0 +1,46 @@ +import {addCardToAppleWallet, checkWalletAvailability, getCardStatus} from '@expensify/react-native-wallet'; +import type {IOSCardData} from '@expensify/react-native-wallet/lib/typescript/src/NativeWallet'; +import {Alert} from 'react-native'; +import {issuerEncryptPayloadCallback} from '@libs/actions/Wallet'; +import Log from '@libs/Log'; +import type {Card} from '@src/types/onyx'; + +function checkIfWalletIsAvailable(): Promise { + return checkWalletAvailability(); +} + +function handleAddCardToWallet(card: Card, cardHolderName: string, cardDescription: string, onFinished?: () => void) { + const data = { + network: 'VISA', + lastDigits: card.lastFourPAN, + cardDescription, + cardHolderName, + } as IOSCardData; + + addCardToAppleWallet(data, issuerEncryptPayloadCallback) + .then(() => { + Log.info('Card added to wallet'); + onFinished?.(); + }) + .catch((e) => { + Log.warn(`handleAddCardToWallet error: ${e}`); + Alert.alert('Failed to add card to wallet', 'Please try again later.'); + }); +} + +function isCardInWallet(card: Card): Promise { + if (card.lastFourPAN) { + return getCardStatus(card.lastFourPAN) + .then((status) => { + Log.info(`Card status: ${status}`); + return status === 'active'; + }) + .catch((e) => { + Log.warn(`isCardInWallet error: ${e}`); + return Promise.resolve(false); + }); + } + return Promise.resolve(false); +} + +export {handleAddCardToWallet, isCardInWallet, checkIfWalletIsAvailable}; diff --git a/src/libs/Wallet/index.ts b/src/libs/Wallet/index.ts new file mode 100644 index 000000000000..7707db63b35c --- /dev/null +++ b/src/libs/Wallet/index.ts @@ -0,0 +1,18 @@ +import type {Card} from '@src/types/onyx'; + +function checkIfWalletIsAvailable(): Promise { + return Promise.resolve(false); +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function handleAddCardToWallet(_card: Card, _cardHolderName: string, _cardDescription: string, _onFinished?: () => void) { + Promise.reject(new Error('Add to wallet is not supported on this platform')); +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function isCardInWallet(_card: Card) { + // Return true for other platforms, so the AddToWalletButton is always hidden + return Promise.resolve(true); +} + +export {handleAddCardToWallet, isCardInWallet, checkIfWalletIsAvailable}; diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index f9e8ef61f057..e0a19056e893 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -1,13 +1,16 @@ +import type {IOSEncryptPayload} from '@expensify/react-native-wallet/lib/typescript/src/NativeWallet'; import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; import type {AcceptWalletTermsParams, AnswerQuestionsForWalletParams, UpdatePersonalDetailsForWalletParams, VerifyIdentityParams} from '@libs/API/parameters'; -import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +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 * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import pkg from '../../../package.json'; import {clearErrors} from './FormActions'; type WalletQuestionAnswer = { @@ -271,6 +274,29 @@ function clearPhysicalCardError(cardID?: string) { }); } +function issuerEncryptPayloadCallback(nonce: string, nonceSignature: string, certificates: string[]): Promise { + // eslint-disable-next-line rulesdir/no-api-side-effects-method, rulesdir/no-api-in-views + return API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.CREATE_DIGITAL_WALLET, { + platform: 'ios', + appVersion: pkg.version, + certificates: JSON.stringify({certificates}), + nonce, + nonceSignature, + }) + .then((response) => { + const data = response as unknown as IOSEncryptPayload; + return { + encryptedPassData: data.encryptedPassData, + activationData: data.activationData, + ephemeralPublicKey: data.ephemeralPublicKey, + } as IOSEncryptPayload; + }) + .catch((e) => { + Log.warn(`issuerEncryptPayloadCallback error: ${e}`); + return {} as IOSEncryptPayload; + }); +} + export { openOnfidoFlow, openInitialSettingsPage, @@ -285,4 +311,5 @@ export { setKYCWallSource, resetWalletAdditionalDetailsDraft, clearPhysicalCardError, + issuerEncryptPayloadCallback, }; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx index f9e575aeec6e..afa09c0ae79f 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx @@ -2,6 +2,7 @@ import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import AddToWalletButton from '@components/AddToWalletButton/index'; import Button from '@components/Button'; import CardPreview from '@components/CardPreview'; import DotIndicatorMessage from '@components/DotIndicatorMessage'; @@ -13,8 +14,10 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import ValidateCodeActionModal from '@components/ValidateCodeActionModal'; import useBeforeRemove from '@hooks/useBeforeRemove'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import {requestValidateCodeAction} from '@libs/actions/User'; import {formatCardExpiration, getDomainCards, maskCard} from '@libs/CardUtils'; @@ -69,6 +72,7 @@ function ExpensifyCardPage({ const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate} = useLocalize(); + const {canUseInAppProvisioning} = usePermissions(); const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(false); const [currentCardID, setCurrentCardID] = useState(-1); const isTravelCard = cardList?.[cardID]?.nameValuePairs?.isTravelCard; @@ -76,6 +80,7 @@ function ExpensifyCardPage({ const domain = cardList?.[cardID]?.domainName ?? ''; const expensifyCardTitle = isTravelCard ? translate('cardPage.expensifyTravelCard') : translate('cardPage.expensifyCard'); const pageTitle = shouldDisplayCardDomain ? expensifyCardTitle : cardList?.[cardID]?.nameValuePairs?.cardTitle ?? expensifyCardTitle; + const {displayName} = useCurrentUserPersonalDetails(); const [isNotFound, setIsNotFound] = useState(false); const cardsToShow = useMemo(() => { @@ -94,6 +99,9 @@ function ExpensifyCardPage({ const virtualCards = useMemo(() => cardsToShow?.filter((card) => card?.nameValuePairs?.isVirtual && !card?.nameValuePairs?.isTravelCard), [cardsToShow]); const travelCards = useMemo(() => cardsToShow?.filter((card) => card?.nameValuePairs?.isVirtual && card?.nameValuePairs?.isTravelCard), [cardsToShow]); const physicalCards = useMemo(() => cardsToShow?.filter((card) => !card?.nameValuePairs?.isVirtual), [cardsToShow]); + const cardToAdd = useMemo(() => { + return virtualCards?.at(0); + }, [virtualCards]); const [cardsDetails, setCardsDetails] = useState>({}); const [isCardDetailsLoading, setIsCardDetailsLoading] = useState>({}); const [cardsDetailsErrors, setCardsDetailsErrors] = useState>({}); @@ -324,6 +332,14 @@ function ExpensifyCardPage({ /> )} + {!!canUseInAppProvisioning && cardToAdd !== undefined && ( + + )} {physicalCards?.some((card) => card?.state === CONST.EXPENSIFY_CARD.STATE.NOT_ACTIVATED) && (