Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
69c1a3b
feat(card): add push provisioning base infrastructure
Brunonascdev Feb 4, 2026
961411f
feat(card): add Google Wallet in-app provisioning
Brunonascdev Feb 4, 2026
130e02f
chore(card): undo unnecessary changes on package.json
Brunonascdev Feb 4, 2026
57660f8
feat(card): bring gas_sponsorship_reserve_balance translation back
Brunonascdev Feb 4, 2026
bd0d76c
fix: add missing barrel files and type error parameter
Brunonascdev Feb 4, 2026
0dec5f6
fix: add missing barrel files and type error parameter
Brunonascdev Feb 4, 2026
d26ff5f
fix: add missing barrel files and type error parameter
Brunonascdev Feb 4, 2026
a14d1ff
Merge branch 'feat/card-push-provisioning-base' of github.com:MetaMas…
Brunonascdev Feb 4, 2026
bc7a0ff
test: add push provisioning tests
Brunonascdev Feb 4, 2026
06d3de5
feat: merge with main
Brunonascdev Feb 5, 2026
6974063
fix(card): race conditiion on moduleLoadPromise
Brunonascdev Feb 5, 2026
e6349b5
Merge branch 'feat/card-push-provisioning-base' into feat/google-in-a…
Brunonascdev Feb 5, 2026
fd05701
feat: merge with main
Brunonascdev Feb 5, 2026
43d948a
feat(card): fix providers test and CI steps
Brunonascdev Feb 6, 2026
8e7af04
chore: fix missing variable on bitrise and build.yml
Brunonascdev Feb 6, 2026
bbeaf31
feat(card): add button with svg translations
Brunonascdev Feb 6, 2026
fa39063
Merge branch 'main' of github.com:MetaMask/metamask-mobile into feat/…
Brunonascdev Feb 10, 2026
fe10c69
chore(card): add TAP_AND_PAY_SDK_SSH_KEY on bitrise.yml for cloning t…
Brunonascdev Feb 10, 2026
c207c2e
chore(card): add values on builds.yml
Brunonascdev Feb 10, 2026
d31cc22
chore(card): hard code tap and pay sdk
Brunonascdev Feb 10, 2026
36e3c25
chore: hard code tap and pay sdk path
Brunonascdev Feb 10, 2026
3a7ae8d
[skip ci] Bump version number to 3679
metamaskbot Feb 10, 2026
f710f99
chore: fix libcrypto error
Brunonascdev Feb 10, 2026
630c68e
[skip ci] Bump version number to 3680
metamaskbot Feb 10, 2026
bc122f3
Merge branch 'feat/google-in-app-provisioning' of github.com:MetaMask…
Brunonascdev Feb 10, 2026
bc00a1f
[skip ci] Bump version number to 3681
metamaskbot Feb 10, 2026
bafba96
chore: use base64 to decode key
Brunonascdev Feb 10, 2026
fda2995
Merge branch 'feat/google-in-app-provisioning' of github.com:MetaMask…
Brunonascdev Feb 10, 2026
a626d44
chore: use base64 to decode key
Brunonascdev Feb 10, 2026
49eceb2
[skip ci] Bump version number to 3684
metamaskbot Feb 10, 2026
d9bda6e
chore: newline fix
Brunonascdev Feb 10, 2026
1a937ba
Merge branch 'feat/google-in-app-provisioning' of github.com:MetaMask…
Brunonascdev Feb 10, 2026
a9eb340
[skip ci] Bump version number to 3687
metamaskbot Feb 10, 2026
8432e5b
chore: merge tap and pay steps
Brunonascdev Feb 10, 2026
680a993
Merge branches 'feat/google-in-app-provisioning' and 'feat/google-in-…
Brunonascdev Feb 10, 2026
a1338ff
[skip ci] Bump version number to 3688
metamaskbot Feb 10, 2026
5980646
feat(card): bring back AddToWalletButton component
Brunonascdev Feb 10, 2026
a7cf3bb
Merge branch 'feat/google-in-app-provisioning' of github.com:MetaMask…
Brunonascdev Feb 10, 2026
21a72aa
[skip ci] Bump version number to 3689
metamaskbot Feb 10, 2026
7bb7921
Revert "[skip ci] Bump version number to 3689"
Brunonascdev Feb 11, 2026
ea4b588
[skip ci] Bump version number to 3695
metamaskbot Feb 11, 2026
63a4c8f
refactor(card): simplify push provisioning
Brunonascdev Feb 13, 2026
f884d17
merge with main
Brunonascdev Feb 13, 2026
635a35b
[skip ci] Bump version number to 3714
metamaskbot Feb 13, 2026
370f59a
Merge branch 'main' of github.com:MetaMask/metamask-mobile into feat/…
Brunonascdev Feb 16, 2026
231a77f
chore: fix android ci/cd changes
Brunonascdev Feb 16, 2026
0382256
merge with main
Brunonascdev Feb 24, 2026
b331b73
[skip ci] Bump version number to 3786
metamaskbot Feb 24, 2026
600078b
merge with main
Brunonascdev Mar 3, 2026
999ceb7
fix(card): adapt locale on addtowalletbutton and undefined fallback o…
Brunonascdev Mar 3, 2026
d4217ff
[skip ci] Bump version number to 3879
metamaskbot Mar 3, 2026
0d2e663
[skip ci] Bump version number to 3882
metamaskbot Mar 3, 2026
8aacf20
[skip ci] Bump version number to 3883
metamaskbot Mar 3, 2026
0708b78
merge with main
Brunonascdev Mar 11, 2026
e85fa90
feat(card): fix phone number validation
Brunonascdev Mar 11, 2026
f8a9292
feat(card): add date validation to provisioning
Brunonascdev Mar 11, 2026
71dad74
[skip ci] Bump version number to 4000
metamaskbot Mar 11, 2026
15fe225
merge with main
Brunonascdev Mar 16, 2026
23bc5d9
feat(card): adjust monavate filter date
Brunonascdev Mar 16, 2026
216ebc8
merge with main
Brunonascdev Mar 26, 2026
63a686e
feat(card): add Success state handler and fix build.yml
Brunonascdev Mar 26, 2026
3fc7feb
chore(card): fix secrets context on tap and pay sdk clone
Brunonascdev Mar 27, 2026
7273028
Merge branch 'main' into feat/google-in-app-provisioning
Brunonascdev Mar 30, 2026
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
32 changes: 32 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ jobs:
outputs:
github_environment: ${{ steps.config.outputs.github_environment }}
secrets_json: ${{ steps.config.outputs.secrets_json }}
tap_and_pay_sdk_repo_ssh: ${{ steps.config.outputs.tap_and_pay_sdk_repo_ssh }}
signing_aws_role: ${{ steps.config.outputs.signing_aws_role }}
signing_aws_secret: ${{ steps.config.outputs.signing_aws_secret }}
signing_android_keystore_path: ${{ steps.config.outputs.signing_android_keystore_path }}
Expand All @@ -110,6 +111,8 @@ jobs:
const build = config.builds['${{ inputs.build_name }}'];
fs.appendFileSync(process.env.GITHUB_OUTPUT, 'github_environment=' + build.github_environment + '\n');
fs.appendFileSync(process.env.GITHUB_OUTPUT, 'secrets_json=' + JSON.stringify(build.secrets || {}) + '\n');
const env = build.env || {};
fs.appendFileSync(process.env.GITHUB_OUTPUT, 'tap_and_pay_sdk_repo_ssh=' + (env.TAP_AND_PAY_SDK_REPO_SSH || '') + '\n');
Comment thread
Brunonascdev marked this conversation as resolved.
const signing = build.signing;
fs.appendFileSync(process.env.GITHUB_OUTPUT, 'signing_aws_role=' + (signing ? signing.aws_role || '' : '') + '\n');
fs.appendFileSync(process.env.GITHUB_OUTPUT, 'signing_aws_secret=' + (signing ? signing.aws_secret || '' : '') + '\n');
Expand Down Expand Up @@ -241,6 +244,35 @@ jobs:
XCODE_VERSION: '16.3'
run: sudo xcode-select -s "/Applications/Xcode_$XCODE_VERSION.app"

# TapAndPay SDK Setup (Android only)
- name: Clone TapAndPay SDK
if: |
matrix.platform == 'android' &&
needs.prepare.outputs.tap_and_pay_sdk_repo_ssh != ''
run: |
if [ -z "$TAP_AND_PAY_SDK_SSH_KEY" ]; then
echo "⚠️ TAP_AND_PAY_SDK_SSH_KEY not set, skipping TapAndPay SDK clone"
exit 0
fi
mkdir -p ~/.ssh
echo "$TAP_AND_PAY_SDK_SSH_KEY" | base64 -d > ~/.ssh/tap_and_pay_key
chmod 600 ~/.ssh/tap_and_pay_key
trap 'rm -f ~/.ssh/tap_and_pay_key' EXIT
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/tap_and_pay_key
echo "📦 Cloning TapAndPay SDK into android/libs/..."
git clone --depth 1 "$TAP_AND_PAY_SDK_REPO_SSH" /tmp/tap-and-pay-sdk
mkdir -p android/libs
cp -r /tmp/tap-and-pay-sdk/* android/libs/
rm -rf /tmp/tap-and-pay-sdk
ssh-add -D
eval "$(ssh-agent -k)"
echo "✅ TapAndPay SDK installed to android/libs/"
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
env:
TAP_AND_PAY_SDK_SSH_KEY: ${{ secrets.TAP_AND_PAY_SDK_SSH_KEY }}
TAP_AND_PAY_SDK_REPO_SSH: ${{ needs.prepare.outputs.tap_and_pay_sdk_repo_ssh }}
Comment thread
Brunonascdev marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.

- name: Apply build config
run: |
# Load env vars from builds.yml (this step only)
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ android/app/.project
android/app/bin/
android/app/gradle*
android/app/_build*
android/libs
.cxx/

# if we ever want to add google services
Expand Down
1 change: 1 addition & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ buildscript {
url(new File(['node', '--print', "require.resolve('@notifee/react-native/package.json')"].execute(null, rootDir).text.trim(), '../android/libs'))
}
maven { url "https://jitpack.io" }
maven { url "file://${rootDir}/libs" }
maven { url "https://cdn.veriff.me/android/" }
}
}
Expand Down
20 changes: 14 additions & 6 deletions app/components/UI/Card/Views/CardHome/CardHome.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5354,6 +5354,8 @@ describe('CardHome Component', () => {

const mockUserDetailsForProvisioning = {
id: 'user-123',
firstName: 'John',
lastName: 'Doe',
addressLine1: '123 Main St',
addressLine2: 'Apt 4B',
city: 'New York',
Expand Down Expand Up @@ -5441,7 +5443,7 @@ describe('CardHome Component', () => {

// Verify userAddress uses physical address fields in provisioning format
expect(options.userAddress).toEqual({
name: 'Card Holder', // Uses default since userDetails doesn't have firstName/lastName
name: 'John Doe', // Derived from KYC userDetails firstName/lastName
addressOne: '123 Main St',
addressTwo: 'Apt 4B',
locality: 'New York',
Expand Down Expand Up @@ -5536,11 +5538,17 @@ describe('CardHome Component', () => {
expect(typeof options.onError).toBe('function');
});

it('uses holderName from cardDetails for provisioning', async () => {
// Given: card with holder name from card status API
it('uses holderName from KYC userDetails for provisioning', async () => {
// Given: card with different holder name, but KYC has specific names
const cardWithHolderName = {
...mockCardDetailsWithHolder,
holderName: 'Jane Smith',
holderName: 'Card API Name',
};

const userDetailsWithName = {
...mockUserDetailsForProvisioning,
firstName: 'Jane',
lastName: 'Smith',
};

setupMockSelectors({ isAuthenticated: true, userLocation: 'us' });
Expand All @@ -5552,7 +5560,7 @@ describe('CardHome Component', () => {
kycStatus: {
verificationState: 'VERIFIED',
userId: 'user-123',
userDetails: mockUserDetailsForProvisioning,
userDetails: userDetailsWithName,
},
});

Expand All @@ -5563,7 +5571,7 @@ describe('CardHome Component', () => {
expect(mockUsePushProvisioning).toHaveBeenCalled();
});

// Then: holderName should come from cardDetails
// Then: holderName should come from KYC userDetails, not cardDetails
const options = getLastCallOptions();
const cardDetails = options.cardDetails as { holderName: string };

Expand Down
7 changes: 4 additions & 3 deletions app/components/UI/Card/Views/CardHome/CardHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ import {
getWalletName,
type ProvisioningError,
} from '../../pushProvisioning';
import { AddToWalletButton } from '@expensify/react-native-wallet';
import { AddToWalletButton } from '../../pushProvisioning/components/AddToWalletButton';
import { CardScreenshotDeterrent } from '../../components/CardScreenshotDeterrent';
import { createPasswordBottomSheetNavigationDetails } from '../../components/PasswordBottomSheet';
import { createViewPinBottomSheetNavigationDetails } from '../../components/ViewPinBottomSheet';
Expand Down Expand Up @@ -273,12 +273,12 @@ const CardHome = () => {
cardDetails
? {
id: cardDetails.id,
holderName: cardDetails.holderName,
holderName: cardholderName,
Comment thread
cursor[bot] marked this conversation as resolved.
panLast4: cardDetails.panLast4,
status: cardDetails.status,
}
: null,
[cardDetails],
[cardDetails, cardholderName],
);

const {
Expand All @@ -288,6 +288,7 @@ const CardHome = () => {
} = usePushProvisioning({
cardDetails: cardDetailsForProvisioning,
userAddress: userAddressForProvisioning,
accountCreatedAt: kycStatus?.userDetails?.createdAt,
onSuccess: () => {
toastRef?.current?.showToast({
variant: ToastVariants.Icon,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,36 +27,25 @@ describe('GalileoCardAdapter', () => {
});

describe('getOpaquePaymentCard', () => {
it('returns successful response with encrypted payload', async () => {
it('returns opaquePaymentCard from SDK response', async () => {
const mockResponse = {
opaquePaymentCard: 'encrypted-opaque-card-data',
cardNetwork: 'MASTERCARD',
lastFourDigits: '1234',
cardholderName: 'John Doe',
cardDescription: 'MetaMask Card',
};
mockCardSDK.createGoogleWalletProvisioningRequest.mockResolvedValue(
mockResponse,
);

const result = await adapter.getOpaquePaymentCard();

expect(result.success).toBe(true);
expect(result.encryptedPayload?.opaquePaymentCard).toBe(
'encrypted-opaque-card-data',
);
expect(result.cardNetwork).toBe('MASTERCARD');
expect(result.lastFourDigits).toBe('1234');
expect(result.cardholderName).toBe('John Doe');
expect(result).toEqual({
opaquePaymentCard: 'encrypted-opaque-card-data',
});
});

it('throws ProvisioningError when opaquePaymentCard is missing', async () => {
// Test edge case where SDK returns invalid data
mockCardSDK.createGoogleWalletProvisioningRequest.mockResolvedValue({
opaquePaymentCard: '',
cardNetwork: 'MASTERCARD',
lastFourDigits: '1234',
cardholderName: 'John Doe',
});

await expect(adapter.getOpaquePaymentCard()).rejects.toThrow();
Expand All @@ -66,9 +55,6 @@ describe('GalileoCardAdapter', () => {
// Test edge case where SDK returns null/undefined - use type assertion for testing
mockCardSDK.createGoogleWalletProvisioningRequest.mockResolvedValue(
undefined as unknown as {
cardNetwork: string;
lastFourDigits: string;
cardholderName: string;
opaquePaymentCard: string;
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,20 @@
* Galileo Card Provider Adapter
*
* Implementation of ICardProviderAdapter for Galileo card issuing platform.
* Handles payload encryption for Google Wallet provisioning.
*
* @see https://docs.galileo-ft.com/pro/docs/setup-for-push-provisioning
*/

import {
CardProviderId,
ProvisioningResponse,
ProvisioningError,
ProvisioningErrorCode,
CardNetwork,
ApplePayEncryptedPayload,
} from '../../types';
import { ICardProviderAdapter } from './ICardProviderAdapter';
import { CardSDK } from '../../../sdk/CardSDK';
import { strings } from '../../../../../../../locales/i18n';

/**
* Galileo Card Provider Adapter
*
* This adapter interfaces with the Galileo API to encrypt card data
* for push provisioning to Google Wallet.
*
* Galileo Push Provisioning Flow:
* 1. App initiates provisioning and gets card ID
* 2. App sends data to Galileo's Create Provisioning Request API
* 3. Galileo encrypts the payload and returns it
* 4. App passes encrypted payload to wallet SDK
*
* @see https://docs.galileo-ft.com/pro/docs/creating-a-provisioning-request
*/
export class GalileoCardAdapter implements ICardProviderAdapter {
readonly providerId: CardProviderId = 'galileo';

Expand All @@ -42,13 +25,7 @@ export class GalileoCardAdapter implements ICardProviderAdapter {
this.cardSDK = cardSDK;
}

/**
* Get pre-encrypted opaque payment card data for Google Wallet
*
* For Google Wallet, we need to send the card ID
* to Galileo, which returns the pre-encrypted opaque payment card data.
*/
async getOpaquePaymentCard(): Promise<ProvisioningResponse> {
async getOpaquePaymentCard(): Promise<{ opaquePaymentCard: string }> {
try {
const response =
await this.cardSDK.createGoogleWalletProvisioningRequest();
Expand All @@ -61,14 +38,7 @@ export class GalileoCardAdapter implements ICardProviderAdapter {
}

return {
success: true,
encryptedPayload: {
opaquePaymentCard: response.opaquePaymentCard,
},
cardNetwork: response.cardNetwork as CardNetwork,
lastFourDigits: response.lastFourDigits,
cardholderName: response.cardholderName,
cardDescription: response.cardDescription,
opaquePaymentCard: response.opaquePaymentCard,
};
} catch (error) {
if (error instanceof ProvisioningError) {
Expand All @@ -83,49 +53,28 @@ export class GalileoCardAdapter implements ICardProviderAdapter {
}
}

/**
* Convert Base64-encoded string to hex-encoded string
*
* PassKit provides data as Base64-encoded strings, but the API
* expects hex-encoded strings.
*
* @param base64 - Base64-encoded string
* @returns Hex-encoded string
*/
/** Convert Base64-encoded string to hex (PassKit provides Base64, API expects hex) */
private base64ToHex(base64: string): string {
// Use Buffer to decode Base64 and convert to hex
return Buffer.from(base64, 'base64').toString('hex');
}

/**
* Get encrypted payload for Apple Pay in-app provisioning
*
* For Apple Pay, PassKit provides cryptographic data (nonce, certificates)
* as Base64-encoded strings. This method converts them to hex and sends
* to Galileo to get the encrypted payload.
*
* @param nonce - Cryptographic nonce from PassKit (Base64-encoded)
* @param nonceSignature - Signature of the nonce from PassKit (Base64-encoded)
* @param certificates - Array of certificate strings from PassKit (Base64-encoded)
* @param certificates[0] - leaf certificate
* @param certificates[1] - intermediate certificate
* @returns Promise resolving to the encrypted Apple Pay payload
* Get encrypted payload for Apple Pay in-app provisioning.
* Converts PassKit's Base64-encoded nonce/certificates to hex before sending to Galileo.
*/
async getApplePayEncryptedPayload(
nonce: string,
nonceSignature: string,
certificates: string[],
): Promise<ApplePayEncryptedPayload> {
try {
// Validate certificates array
if (!certificates || certificates.length < 2) {
throw new ProvisioningError(
ProvisioningErrorCode.ENCRYPTION_FAILED,
strings('card.push_provisioning.error_encryption_failed'),
);
}

// Convert Base64 to hex as required by the API
const leafCertificate = this.base64ToHex(certificates[0]);
const intermediateCertificate = this.base64ToHex(certificates[1]);
const nonceHex = this.base64ToHex(nonce);
Expand Down
Loading
Loading