Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## Unreleased
**Fixes**
* [Fixed] Manual bank-account entry in ConnectAccountOnboarding now creates an external account on iOS/Android by collecting a bank-account token instead of only Financial Connections accounts.

## 0.67.0 - 2026-06-09
**Changes**
* Updated Stripe iOS SDK from 25.16.0 to 25.17.0.
Expand Down
1 change: 1 addition & 0 deletions etc/stripe-react-native.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1013,6 +1013,7 @@ export const collectBankAccountToken: (clientSecret: string, params?: CollectBan
type CollectBankAccountTokenParams = {
style?: UserInterfaceStyle;
onEvent?: (event: FinancialConnectionsEvent) => void;
connectedAccountId?: string;
};

// @public
Expand Down
57 changes: 47 additions & 10 deletions src/connect/EmbeddedComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
StripeConnectInitParams,
} from './connectTypes';
import type { FinancialConnections } from '../types';
import { FinancialConnectionsSheetError } from '../types/FinancialConnections';
import { ComponentAnalyticsClient } from './analytics/ComponentAnalyticsClient';

const DEVELOPMENT_MODE = false;
Expand Down Expand Up @@ -457,7 +458,7 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
id: string,
result: {
session?: FinancialConnections.Session;
token?: FinancialConnections.BankAccountToken;
token?: ReturnType<typeof toStripeJsBankAccountToken> | null;
error?: {
code: string;
message: string;
Expand Down Expand Up @@ -580,14 +581,21 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
cleanup,
};

// Call native Financial Connections
NativeStripeSdk.collectFinancialConnectionsAccounts(clientSecret, {
NativeStripeSdk.collectBankAccountToken(clientSecret, {
connectedAccountId,
})
.then(({ session, error }) => {
.then(({ session, token, error }) => {
cleanup();

if (error) {
if (error.code === FinancialConnectionsSheetError.Canceled) {
handleFinancialConnectionsResult(id, {
session: undefined,
token: undefined,
error: undefined,
});
return;
}
handleFinancialConnectionsResult(id, {
session: undefined,
token: undefined,
Expand All @@ -598,21 +606,18 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
type: error.type,
},
});
} else if (session) {
// Note: collectFinancialConnectionsAccounts doesn't return a token
// Only collectBankAccountToken returns both session and token
} else if (token || session) {
handleFinancialConnectionsResult(id, {
session,
token: undefined,
token: token ? toStripeJsBankAccountToken(token) : null,
error: undefined,
});
} else {
// Defensive: should never happen
handleFinancialConnectionsResult(id, {
error: {
code: 'UnexpectedError',
message:
'No session or error returned from Financial Connections',
'No session, token, or error returned from Financial Connections',
},
});
}
Expand Down Expand Up @@ -818,6 +823,38 @@ function withDefaultFontFamily(appearance: any) {
};
}

// connect-js expects the Stripe.js API-shaped (snake_case) token, matching what the
// native iOS/Android Connect SDKs send. RN's FinancialConnections token is camelCase.
/** @internal Exported for testing only. */
export function toStripeJsBankAccountToken(
token: FinancialConnections.BankAccountToken
) {
const ba = token.bankAccount;
return {
id: token.id,
object: 'token' as const,
type: 'bank_account' as const,
used: token.used,
livemode: token.livemode,
created: token.created,
bank_account: ba
? {
id: ba.id,
object: 'bank_account' as const,
account_holder_name: ba.accountHolderName,
account_holder_type: ba.accountHolderType,
bank_name: ba.bankName,
country: ba.country,
currency: ba.currency,
fingerprint: ba.fingerprint,
last4: ba.last4,
routing_number: ba.routingNumber,
status: ba.status,
}
: null,
};
}

// Validates that a URL is well-formed and uses http or https protocol
function isValidUrl(url: string): boolean {
try {
Expand Down
104 changes: 103 additions & 1 deletion src/connect/__tests__/EmbeddedComponent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ jest.mock('../../specs/NativeStripeSdkModule', () =>
import React from 'react';
import { render, waitFor, act } from '@testing-library/react-native';
import { Platform, AppState } from 'react-native';
import { EmbeddedComponent } from '../EmbeddedComponent';
import {
EmbeddedComponent,
toStripeJsBankAccountToken,
} from '../EmbeddedComponent';
import {
loadConnectAndInitialize,
ConnectComponentsProvider,
Expand Down Expand Up @@ -338,4 +341,103 @@ describe('EmbeddedComponent', () => {
expect(contextValue.appearance.variables.fontFamily).toBe('CustomFont');
});
});

describe('toStripeJsBankAccountToken', () => {
it('maps camelCase token to snake_case Stripe.js shape', () => {
const result = toStripeJsBankAccountToken({
id: 'tok_123',
livemode: false,
used: false,
type: 'BankAccount',
created: 1000000,
bankAccount: {
id: 'ba_123',
bankName: 'Test Bank',
accountHolderName: 'John Doe',
accountHolderType: 'Individual',
currency: 'usd',
country: 'US',
routingNumber: '110000000',
status: 'New',
fingerprint: 'fp_123',
last4: '6789',
},
});

expect(result).toEqual({
id: 'tok_123',
object: 'token',
type: 'bank_account',
used: false,
livemode: false,
created: 1000000,
bank_account: {
id: 'ba_123',
object: 'bank_account',
account_holder_name: 'John Doe',
account_holder_type: 'Individual',
bank_name: 'Test Bank',
country: 'US',
currency: 'usd',
fingerprint: 'fp_123',
last4: '6789',
routing_number: '110000000',
status: 'New',
},
});
});

it('returns null bank_account when bankAccount is null', () => {
const result = toStripeJsBankAccountToken({
id: 'tok_456',
livemode: true,
used: true,
type: 'BankAccount',
created: 2000000,
bankAccount: null,
});

expect(result.id).toBe('tok_456');
expect(result.object).toBe('token');
expect(result.type).toBe('bank_account');
expect(result.livemode).toBe(true);
expect(result.bank_account).toBeNull();
});

it('preserves null fields in bank_account', () => {
const result = toStripeJsBankAccountToken({
id: 'tok_789',
livemode: false,
used: false,
type: 'BankAccount',
created: null,
bankAccount: {
id: 'ba_789',
bankName: null,
accountHolderName: null,
accountHolderType: null,
currency: null,
country: null,
routingNumber: null,
status: null,
fingerprint: null,
last4: null,
},
});

expect(result.bank_account).toEqual({
id: 'ba_789',
object: 'bank_account',
account_holder_name: null,
account_holder_type: null,
bank_name: null,
country: null,
currency: null,
fingerprint: null,
last4: null,
routing_number: null,
status: null,
});
});
});
});
2 changes: 2 additions & 0 deletions src/types/PaymentMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,4 +304,6 @@ export type CollectBankAccountTokenParams = {
style?: UserInterfaceStyle;
/** An optional event listener to receive @type {FinancialConnectionEvent} for specific events during the process of a user connecting their financial accounts. */
onEvent?: (event: FinancialConnectionsEvent) => void;
/** Optional connected account ID. Used for Stripe Connect embedded components. */
connectedAccountId?: string;
};
Loading