Skip to content

Commit 57e5448

Browse files
committed
[INTER-2970] Fastlane implementation
1 parent bbe75d6 commit 57e5448

23 files changed

+708
-20
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"author": "",
2828
"devDependencies": {
2929
"@types/jest": "^28.1.2",
30+
"@types/node": "^20.12.6",
3031
"@typescript-eslint/eslint-plugin": "^4.33.0",
3132
"@typescript-eslint/parser": "^4.33.0",
3233
"eslint": "^7.32.0",
@@ -36,6 +37,7 @@
3637
"jest-junit": "^14.0.0",
3738
"lint-staged": "^11.1.2",
3839
"ts-jest": "^28.0.5",
40+
"ts-node": "^10.9.2",
3941
"ttypescript": "^1.5.12",
4042
"typedoc": "^0.22.18",
4143
"typedoc-plugin-markdown": "^3.13.6",

src/braintree/getBraintreeJsUrls.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
import {braintreeConstants, IBraintreeUrls} from 'src';
22

3-
export function getBraintreeJsUrls(): IBraintreeUrls {
3+
/**
4+
* @param version If provided, URLs will be built with this version instead
5+
*/
6+
export function getBraintreeJsUrls(version?: string): IBraintreeUrls {
47
const {
58
BASE_JS_URL: base,
69
APPLE_JS: appleJs,
710
GOOGLE_JS: googleJs,
811
CLIENT_JS: clientJs,
12+
FASTLANE_JS: fastlaneJs,
913
DATA_COLLECTOR_JS: dataCollectorJs,
1014
GOOGLE_JS_URL: googleJsUrl,
1115
JS_VERSION: jsVersion
1216
} = braintreeConstants;
13-
const clientJsURL = `${base}/${jsVersion}/${clientJs}`;
14-
const appleJsURL = `${base}/${jsVersion}/${appleJs}`;
15-
const braintreeGoogleJsURL = `${base}/${jsVersion}/${googleJs}`;
16-
const dataCollectorJsURL = `${base}/${jsVersion}/${dataCollectorJs}`;
17+
version ??= jsVersion;
18+
const clientJsURL = `${base}/${version}/${clientJs}`;
19+
const appleJsURL = `${base}/${version}/${appleJs}`;
20+
const braintreeGoogleJsURL = `${base}/${version}/${googleJs}`;
21+
const dataCollectorJsURL = `${base}/${version}/${dataCollectorJs}`;
22+
const fastlaneJsURL = `${base}/${version}/${fastlaneJs}`;
1723

18-
return {appleJsURL, clientJsURL, dataCollectorJsURL, googleJsUrl, braintreeGoogleJsURL};
24+
return {appleJsURL, clientJsURL, dataCollectorJsURL, googleJsUrl, braintreeGoogleJsURL, fastlaneJsURL};
1925
}

src/fastlane/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './initFastlane';
2+
export * from './manageFastlaneState';

src/fastlane/initFastlane.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { getPublicOrderId, getEnvironment, getShopIdentifier, getJwtToken } from '@boldcommerce/checkout-frontend-library';
2+
import { loadScript } from '@paypal/paypal-js';
3+
import {
4+
loadJS,
5+
getBraintreeJsUrls,
6+
braintreeOnLoadClient,
7+
IFastlaneInstance,
8+
getBraintreeClient,
9+
IBraintreeClient,
10+
FastlaneLoadingError,
11+
} from 'src';
12+
13+
interface TokenResponse {
14+
is_test_mode: boolean;
15+
client_token: string;
16+
}
17+
18+
interface BraintreeTokenResponse extends TokenResponse {
19+
type: 'braintree';
20+
client_id: null;
21+
}
22+
23+
interface PPCPTokenResponse extends TokenResponse {
24+
type: 'ppcp';
25+
client_id: string;
26+
}
27+
28+
export async function initFastlane(): Promise<IFastlaneInstance> {
29+
const {clientJsURL, dataCollectorJsURL, fastlaneJsURL} = getBraintreeJsUrls('3.101.0-fastlane-beta.7.2');
30+
31+
try {
32+
// TODO move this request to the checkout frontend library
33+
const env = getEnvironment();
34+
const shopId = getShopIdentifier();
35+
const publicOrderId = getPublicOrderId();
36+
const jwt = getJwtToken();
37+
const resp = await fetch(`${env.url}/checkout/storefront/${shopId}/${publicOrderId}/paypal_fastlane/client_token`, {
38+
headers: {
39+
Authorization: `Bearer ${jwt}`,
40+
},
41+
});
42+
43+
// Getting client token and which SDK to use
44+
const {
45+
client_token: clientToken,
46+
client_id: clientId,
47+
type,
48+
is_test_mode: isTest,
49+
} = await resp.json().then(r => r.data) as BraintreeTokenResponse | PPCPTokenResponse;
50+
51+
switch (type) {
52+
case 'braintree': {
53+
await Promise.all([
54+
loadJS(clientJsURL),
55+
loadJS(fastlaneJsURL),
56+
loadJS(dataCollectorJsURL),
57+
]).then(braintreeOnLoadClient);
58+
59+
const braintree = getBraintreeClient() as IBraintreeClient;
60+
const client = await braintree.client.create({authorization: clientToken});
61+
const dataCollector = await braintree.dataCollector.create({
62+
client: client,
63+
riskCorrelationId: getPublicOrderId(),
64+
});
65+
const fastlane = await braintree.fastlane.create({
66+
client,
67+
authorization: clientToken,
68+
deviceData: dataCollector.deviceData,
69+
});
70+
71+
return fastlane;
72+
}
73+
case 'ppcp': {
74+
const paypal = await loadScript({
75+
dataUserIdToken: clientToken,
76+
clientId: clientId,
77+
components: 'fastlane',
78+
debug: isTest,
79+
}) as unknown as {Fastlane: () => Promise<IFastlaneInstance>};
80+
const fastlane = await paypal.Fastlane();
81+
82+
return fastlane;
83+
}
84+
default:
85+
throw new Error(`unknown type: ${type}`);
86+
}
87+
} catch (error) {
88+
if (error instanceof Error) {
89+
error.name = FastlaneLoadingError.name;
90+
throw error;
91+
}
92+
93+
throw new FastlaneLoadingError(`Error loading Fastlane: ${error}`);
94+
}
95+
}

src/fastlane/manageFastlaneState.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {IFastlaneInstance} from 'src/types';
2+
import {fastlaneState} from 'src/variables';
3+
import {initFastlane} from './initFastlane';
4+
5+
/**
6+
* Gets an instance of Fastlane. If the instance has not yet been initialized then
7+
* one will be initialized and returned asynchronously. Calls to `getFastlaneInstance` while
8+
* and instance is being initialized will return the same promise, avoiding duplicate initializations
9+
* of the Fastlane instance.
10+
*/
11+
export const getFastlaneInstance = async (): Promise<IFastlaneInstance> => {
12+
return fastlaneState.instance ?? (fastlaneState.instance = initFastlane().catch((e) => {
13+
// Clearing the rejected promise from state so we can try again
14+
fastlaneState.instance = null;
15+
throw e;
16+
}));
17+
};

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from './types';
77
export * from './utils';
88
export * from './variables';
99
export * from './utils';
10+
export * from './fastlane';

src/initialize/initialize.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ export function initialize(props: IInitializeProps): void{
2121
const {alternative_payment_methods} = getOrderInitialData();
2222
setOnAction(props.onAction);
2323

24-
if(alternative_payment_methods){
25-
alternative_payment_methods.forEach(paymentMethod => {
24+
if (alternative_payment_methods){
25+
for (const paymentMethod of alternative_payment_methods) {
2626
const type = paymentMethod.type;
2727
switch (type){
2828
case alternatePaymentMethodType.STRIPE:
@@ -45,6 +45,6 @@ export function initialize(props: IInitializeProps): void{
4545
console.log('do nothing'); // TODO Implement the default behaviour.
4646
break;
4747
}
48-
});
48+
}
4949
}
5050
}

src/types/braintree.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {IFastlaneInstance} from './fastlane';
12
import ApplePayPaymentRequest = ApplePayJS.ApplePayPaymentRequest;
23
import ApplePayPaymentToken = ApplePayJS.ApplePayPaymentToken;
34
import GooglePaymentData = google.payments.api.PaymentData;
@@ -10,12 +11,35 @@ export interface IBraintreeClient {
1011
};
1112
applePay: {
1213
create: IBraintreeApplePayCreate
13-
}
14+
};
1415
googlePayment: {
1516
create: IBraintreeGooglePayCreate
16-
}
17+
};
18+
dataCollector: {
19+
create: (_: {
20+
client: IBraintreeClientInstance;
21+
riskCorrelationId?: string;
22+
}) => Promise<IBraintreeDataCollectorInstance>;
23+
};
24+
fastlane: {
25+
create: IBraintreeFastlaneCreate;
26+
};
1727
}
1828

29+
export interface IBraintreeFastlaneCreateRequest {
30+
authorization: string;
31+
client: IBraintreeClientInstance;
32+
deviceData: unknown;
33+
metadata?: {
34+
geoLocOverride: string;
35+
};
36+
}
37+
38+
export interface IBraintreeDataCollectorCreateRequest {
39+
client: IBraintreeClientInstance;
40+
riskCorrelationId?: string;
41+
}
42+
1943
export interface IBraintreeClientCreateRequest {
2044
authorization: string;
2145
}
@@ -81,13 +105,19 @@ export interface IBraintreeApplePayPaymentAuthorizedResponse {
81105
}
82106
}
83107

108+
export interface IBraintreeDataCollectorInstance {
109+
deviceData: unknown;
110+
}
111+
84112
export type IBraintreeRequiredContactField = Array<'postalAddress' | 'email' | 'phone'>;
85113
export type IBraintreeClientInstance = Record<string, unknown>;
86114
export type IBraintreeClientCreateCallback = (error: string | Error | undefined, instance: IBraintreeClientInstance) => void;
87115
export type IBraintreeApplePayCreateCallback = (error: string | Error | undefined, instance: IBraintreeApplePayInstance) => void;
88116
export type IBraintreeGooglePayCreateCallback = (error: string | Error | undefined, instance: IBraintreeGooglePayInstance) => void;
89117
export type IBraintreeApplePayPerformValidationCallback = (error: string | Error | undefined, merchantSession: unknown) => void;
90118
export type IBraintreeApplePayPaymentAuthorizedCallback = (error: string | Error | undefined, payload: IBraintreeApplePayPaymentAuthorizedResponse | undefined) => void;
91-
export type IBraintreeClientCreate = (request: IBraintreeClientCreateRequest, callback?: IBraintreeClientCreateCallback) => IBraintreeClientInstance;
119+
export type IBraintreeClientCreate = (request: IBraintreeClientCreateRequest, callback?: IBraintreeClientCreateCallback) => Promise<IBraintreeClientInstance>;
92120
export type IBraintreeApplePayCreate = (request: IBraintreeApplePayCreateRequest, callback?: IBraintreeApplePayCreateCallback) => IBraintreeApplePayInstance;
93-
export type IBraintreeGooglePayCreate = (request: IBraintreeGooglePayCreateRequest, callback?: IBraintreeGooglePayCreateCallback) => IBraintreeGooglePayInstance;
121+
export type IBraintreeGooglePayCreate = (request: IBraintreeGooglePayCreateRequest, callback?: IBraintreeGooglePayCreateCallback) => Promise<IBraintreeGooglePayInstance>;
122+
export type IBraintreeFastlaneCreate = (request: IBraintreeFastlaneCreateRequest) => Promise<IFastlaneInstance>;
123+
export type IBraintreeDataCollectorCreate = (request: IBraintreeDataCollectorCreateRequest) => Promise<IBraintreeDataCollectorInstance>;

src/types/errors.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ export class GooglePayLoadingError extends Error {
1414
}
1515
}
1616

17+
export class FastlaneLoadingError extends Error {
18+
constructor(message: string) {
19+
super(message);
20+
}
21+
}
22+
1723
export class ApplePayValidateMerchantError extends Error {
1824
constructor(message: string) {
1925
super(message);

src/types/fastlane.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
interface IFastlaneAddress {
2+
firstName: string;
3+
lastName: string;
4+
company?: string;
5+
streetAddress: string;
6+
extendedAddress?: string;
7+
locality: string; // City
8+
region: string; // State
9+
postalCode: string;
10+
countryCodeNumeric?: number;
11+
countryCodeAlpha2: string;
12+
countryCodeAlpha3?: string;
13+
phoneNumber: string;
14+
}
15+
16+
export interface IFastlanePaymentToken {
17+
id: string;
18+
paymentSource: {
19+
card: {
20+
brand: string;
21+
expiry: string; // "YYYY-MM"
22+
lastDigits: string; // "1111"
23+
name: string;
24+
billingAddress: IFastlaneAddress;
25+
}
26+
}
27+
}
28+
29+
export interface IFastlanePaymentComponent {
30+
render: (container: string) => IFastlanePaymentComponent;
31+
getPaymentToken: () => Promise<IFastlanePaymentToken>;
32+
setShippingAddress: (shippingAddress: IFastlaneAddress) => void;
33+
}
34+
35+
export interface IFastlaneCardComponent {
36+
render: (container: string) => IFastlaneCardComponent;
37+
getPaymentToken: (options: {
38+
billingAddress: IFastlaneAddress;
39+
}) => Promise<IFastlanePaymentToken>;
40+
}
41+
42+
interface Field {
43+
placeholder?: string;
44+
prefill?: string;
45+
}
46+
47+
export interface IFastlaneComponentOptions {
48+
styles?: unknown;
49+
fields?: {
50+
number?: Field;
51+
expirationDate?: Field;
52+
expirationMonth?: Field;
53+
expirationYear?: Field
54+
cvv?: Field;
55+
postalCode?: Field;
56+
cardholderName?: Field;
57+
phoneNumber?: Field;
58+
};
59+
shippingAddress?: IFastlaneAddress;
60+
}
61+
62+
export interface IFastlaneAuthenticatedCustomerResult {
63+
authenticationState: 'succeeded'|'failed'|'canceled'|'not_found';
64+
profileData: {
65+
name: {
66+
firstName: string;
67+
lastName: string;
68+
};
69+
shippingAddress: IFastlaneAddress;
70+
card: IFastlanePaymentToken;
71+
}
72+
}
73+
74+
export interface IFastlaneInstance {
75+
profile: {
76+
showShippingAddressSelector: () => Promise<{
77+
selectionChanged: true;
78+
selectedAddress: IFastlaneAddress;
79+
} | {
80+
selectionChanged: false;
81+
selectedAddress: null;
82+
}>;
83+
showCardSelector: () => Promise<{
84+
selectionChanged: true;
85+
selectedCard: IFastlanePaymentToken;
86+
} | {
87+
selectionChanged: false;
88+
selectedCard: null;
89+
}>;
90+
};
91+
setLocale: (locale: string) => void;
92+
identity: {
93+
lookupCustomerByEmail: (email: string) => Promise<{customerContextId: string}>;
94+
triggerAuthenticationFlow: (customerContextId: string) => Promise<IFastlaneAuthenticatedCustomerResult>
95+
};
96+
FastlanePaymentComponent: (options: IFastlaneComponentOptions) => Promise<IFastlanePaymentComponent>;
97+
FastlaneCardComponent: (options: Omit<IFastlaneComponentOptions, 'shippingAddress'>) => IFastlaneCardComponent;
98+
}

0 commit comments

Comments
 (0)