Skip to content

Commit

Permalink
[INTER-2970] Fastlane implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
BoldCole committed Apr 11, 2024
1 parent bbe75d6 commit 57e5448
Show file tree
Hide file tree
Showing 23 changed files with 708 additions and 20 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"author": "",
"devDependencies": {
"@types/jest": "^28.1.2",
"@types/node": "^20.12.6",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"eslint": "^7.32.0",
Expand All @@ -36,6 +37,7 @@
"jest-junit": "^14.0.0",
"lint-staged": "^11.1.2",
"ts-jest": "^28.0.5",
"ts-node": "^10.9.2",
"ttypescript": "^1.5.12",
"typedoc": "^0.22.18",
"typedoc-plugin-markdown": "^3.13.6",
Expand Down
18 changes: 12 additions & 6 deletions src/braintree/getBraintreeJsUrls.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import {braintreeConstants, IBraintreeUrls} from 'src';

export function getBraintreeJsUrls(): IBraintreeUrls {
/**
* @param version If provided, URLs will be built with this version instead
*/
export function getBraintreeJsUrls(version?: string): IBraintreeUrls {
const {
BASE_JS_URL: base,
APPLE_JS: appleJs,
GOOGLE_JS: googleJs,
CLIENT_JS: clientJs,
FASTLANE_JS: fastlaneJs,
DATA_COLLECTOR_JS: dataCollectorJs,
GOOGLE_JS_URL: googleJsUrl,
JS_VERSION: jsVersion
} = braintreeConstants;
const clientJsURL = `${base}/${jsVersion}/${clientJs}`;
const appleJsURL = `${base}/${jsVersion}/${appleJs}`;
const braintreeGoogleJsURL = `${base}/${jsVersion}/${googleJs}`;
const dataCollectorJsURL = `${base}/${jsVersion}/${dataCollectorJs}`;
version ??= jsVersion;
const clientJsURL = `${base}/${version}/${clientJs}`;
const appleJsURL = `${base}/${version}/${appleJs}`;
const braintreeGoogleJsURL = `${base}/${version}/${googleJs}`;
const dataCollectorJsURL = `${base}/${version}/${dataCollectorJs}`;
const fastlaneJsURL = `${base}/${version}/${fastlaneJs}`;

return {appleJsURL, clientJsURL, dataCollectorJsURL, googleJsUrl, braintreeGoogleJsURL};
return {appleJsURL, clientJsURL, dataCollectorJsURL, googleJsUrl, braintreeGoogleJsURL, fastlaneJsURL};
}
2 changes: 2 additions & 0 deletions src/fastlane/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './initFastlane';
export * from './manageFastlaneState';
95 changes: 95 additions & 0 deletions src/fastlane/initFastlane.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { getPublicOrderId, getEnvironment, getShopIdentifier, getJwtToken } from '@boldcommerce/checkout-frontend-library';
import { loadScript } from '@paypal/paypal-js';
import {
loadJS,
getBraintreeJsUrls,
braintreeOnLoadClient,
IFastlaneInstance,
getBraintreeClient,
IBraintreeClient,
FastlaneLoadingError,
} from 'src';

interface TokenResponse {
is_test_mode: boolean;
client_token: string;
}

interface BraintreeTokenResponse extends TokenResponse {
type: 'braintree';
client_id: null;
}

interface PPCPTokenResponse extends TokenResponse {
type: 'ppcp';
client_id: string;
}

export async function initFastlane(): Promise<IFastlaneInstance> {
const {clientJsURL, dataCollectorJsURL, fastlaneJsURL} = getBraintreeJsUrls('3.101.0-fastlane-beta.7.2');

try {
// TODO move this request to the checkout frontend library
const env = getEnvironment();
const shopId = getShopIdentifier();
const publicOrderId = getPublicOrderId();
const jwt = getJwtToken();
const resp = await fetch(`${env.url}/checkout/storefront/${shopId}/${publicOrderId}/paypal_fastlane/client_token`, {
headers: {
Authorization: `Bearer ${jwt}`,
},
});

// Getting client token and which SDK to use
const {
client_token: clientToken,
client_id: clientId,
type,
is_test_mode: isTest,
} = await resp.json().then(r => r.data) as BraintreeTokenResponse | PPCPTokenResponse;

switch (type) {
case 'braintree': {
await Promise.all([
loadJS(clientJsURL),
loadJS(fastlaneJsURL),
loadJS(dataCollectorJsURL),
]).then(braintreeOnLoadClient);

const braintree = getBraintreeClient() as IBraintreeClient;
const client = await braintree.client.create({authorization: clientToken});
const dataCollector = await braintree.dataCollector.create({
client: client,
riskCorrelationId: getPublicOrderId(),
});
const fastlane = await braintree.fastlane.create({
client,
authorization: clientToken,
deviceData: dataCollector.deviceData,
});

return fastlane;
}
case 'ppcp': {
const paypal = await loadScript({
dataUserIdToken: clientToken,
clientId: clientId,
components: 'fastlane',
debug: isTest,
}) as unknown as {Fastlane: () => Promise<IFastlaneInstance>};
const fastlane = await paypal.Fastlane();

return fastlane;
}
default:
throw new Error(`unknown type: ${type}`);
}
} catch (error) {
if (error instanceof Error) {
error.name = FastlaneLoadingError.name;
throw error;
}

throw new FastlaneLoadingError(`Error loading Fastlane: ${error}`);
}
}
17 changes: 17 additions & 0 deletions src/fastlane/manageFastlaneState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {IFastlaneInstance} from 'src/types';
import {fastlaneState} from 'src/variables';
import {initFastlane} from './initFastlane';

/**
* Gets an instance of Fastlane. If the instance has not yet been initialized then
* one will be initialized and returned asynchronously. Calls to `getFastlaneInstance` while
* and instance is being initialized will return the same promise, avoiding duplicate initializations
* of the Fastlane instance.
*/
export const getFastlaneInstance = async (): Promise<IFastlaneInstance> => {
return fastlaneState.instance ?? (fastlaneState.instance = initFastlane().catch((e) => {
// Clearing the rejected promise from state so we can try again
fastlaneState.instance = null;
throw e;
}));
};
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './types';
export * from './utils';
export * from './variables';
export * from './utils';
export * from './fastlane';
6 changes: 3 additions & 3 deletions src/initialize/initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export function initialize(props: IInitializeProps): void{
const {alternative_payment_methods} = getOrderInitialData();
setOnAction(props.onAction);

if(alternative_payment_methods){
alternative_payment_methods.forEach(paymentMethod => {
if (alternative_payment_methods){
for (const paymentMethod of alternative_payment_methods) {
const type = paymentMethod.type;
switch (type){
case alternatePaymentMethodType.STRIPE:
Expand All @@ -45,6 +45,6 @@ export function initialize(props: IInitializeProps): void{
console.log('do nothing'); // TODO Implement the default behaviour.
break;
}
});
}
}
}
38 changes: 34 additions & 4 deletions src/types/braintree.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {IFastlaneInstance} from './fastlane';
import ApplePayPaymentRequest = ApplePayJS.ApplePayPaymentRequest;
import ApplePayPaymentToken = ApplePayJS.ApplePayPaymentToken;
import GooglePaymentData = google.payments.api.PaymentData;
Expand All @@ -10,12 +11,35 @@ export interface IBraintreeClient {
};
applePay: {
create: IBraintreeApplePayCreate
}
};
googlePayment: {
create: IBraintreeGooglePayCreate
}
};
dataCollector: {
create: (_: {
client: IBraintreeClientInstance;
riskCorrelationId?: string;
}) => Promise<IBraintreeDataCollectorInstance>;
};
fastlane: {
create: IBraintreeFastlaneCreate;
};
}

export interface IBraintreeFastlaneCreateRequest {
authorization: string;
client: IBraintreeClientInstance;
deviceData: unknown;
metadata?: {
geoLocOverride: string;
};
}

export interface IBraintreeDataCollectorCreateRequest {
client: IBraintreeClientInstance;
riskCorrelationId?: string;
}

export interface IBraintreeClientCreateRequest {
authorization: string;
}
Expand Down Expand Up @@ -81,13 +105,19 @@ export interface IBraintreeApplePayPaymentAuthorizedResponse {
}
}

export interface IBraintreeDataCollectorInstance {
deviceData: unknown;
}

export type IBraintreeRequiredContactField = Array<'postalAddress' | 'email' | 'phone'>;
export type IBraintreeClientInstance = Record<string, unknown>;
export type IBraintreeClientCreateCallback = (error: string | Error | undefined, instance: IBraintreeClientInstance) => void;
export type IBraintreeApplePayCreateCallback = (error: string | Error | undefined, instance: IBraintreeApplePayInstance) => void;
export type IBraintreeGooglePayCreateCallback = (error: string | Error | undefined, instance: IBraintreeGooglePayInstance) => void;
export type IBraintreeApplePayPerformValidationCallback = (error: string | Error | undefined, merchantSession: unknown) => void;
export type IBraintreeApplePayPaymentAuthorizedCallback = (error: string | Error | undefined, payload: IBraintreeApplePayPaymentAuthorizedResponse | undefined) => void;
export type IBraintreeClientCreate = (request: IBraintreeClientCreateRequest, callback?: IBraintreeClientCreateCallback) => IBraintreeClientInstance;
export type IBraintreeClientCreate = (request: IBraintreeClientCreateRequest, callback?: IBraintreeClientCreateCallback) => Promise<IBraintreeClientInstance>;
export type IBraintreeApplePayCreate = (request: IBraintreeApplePayCreateRequest, callback?: IBraintreeApplePayCreateCallback) => IBraintreeApplePayInstance;
export type IBraintreeGooglePayCreate = (request: IBraintreeGooglePayCreateRequest, callback?: IBraintreeGooglePayCreateCallback) => IBraintreeGooglePayInstance;
export type IBraintreeGooglePayCreate = (request: IBraintreeGooglePayCreateRequest, callback?: IBraintreeGooglePayCreateCallback) => Promise<IBraintreeGooglePayInstance>;
export type IBraintreeFastlaneCreate = (request: IBraintreeFastlaneCreateRequest) => Promise<IFastlaneInstance>;
export type IBraintreeDataCollectorCreate = (request: IBraintreeDataCollectorCreateRequest) => Promise<IBraintreeDataCollectorInstance>;
6 changes: 6 additions & 0 deletions src/types/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ export class GooglePayLoadingError extends Error {
}
}

export class FastlaneLoadingError extends Error {
constructor(message: string) {
super(message);
}
}

export class ApplePayValidateMerchantError extends Error {
constructor(message: string) {
super(message);
Expand Down
98 changes: 98 additions & 0 deletions src/types/fastlane.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
interface IFastlaneAddress {
firstName: string;
lastName: string;
company?: string;
streetAddress: string;
extendedAddress?: string;
locality: string; // City
region: string; // State
postalCode: string;
countryCodeNumeric?: number;
countryCodeAlpha2: string;
countryCodeAlpha3?: string;
phoneNumber: string;
}

export interface IFastlanePaymentToken {
id: string;
paymentSource: {
card: {
brand: string;
expiry: string; // "YYYY-MM"
lastDigits: string; // "1111"
name: string;
billingAddress: IFastlaneAddress;
}
}
}

export interface IFastlanePaymentComponent {
render: (container: string) => IFastlanePaymentComponent;
getPaymentToken: () => Promise<IFastlanePaymentToken>;
setShippingAddress: (shippingAddress: IFastlaneAddress) => void;
}

export interface IFastlaneCardComponent {
render: (container: string) => IFastlaneCardComponent;
getPaymentToken: (options: {
billingAddress: IFastlaneAddress;
}) => Promise<IFastlanePaymentToken>;
}

interface Field {
placeholder?: string;
prefill?: string;
}

export interface IFastlaneComponentOptions {
styles?: unknown;
fields?: {
number?: Field;
expirationDate?: Field;
expirationMonth?: Field;
expirationYear?: Field
cvv?: Field;
postalCode?: Field;
cardholderName?: Field;
phoneNumber?: Field;
};
shippingAddress?: IFastlaneAddress;
}

export interface IFastlaneAuthenticatedCustomerResult {
authenticationState: 'succeeded'|'failed'|'canceled'|'not_found';
profileData: {
name: {
firstName: string;
lastName: string;
};
shippingAddress: IFastlaneAddress;
card: IFastlanePaymentToken;
}
}

export interface IFastlaneInstance {
profile: {
showShippingAddressSelector: () => Promise<{
selectionChanged: true;
selectedAddress: IFastlaneAddress;
} | {
selectionChanged: false;
selectedAddress: null;
}>;
showCardSelector: () => Promise<{
selectionChanged: true;
selectedCard: IFastlanePaymentToken;
} | {
selectionChanged: false;
selectedCard: null;
}>;
};
setLocale: (locale: string) => void;
identity: {
lookupCustomerByEmail: (email: string) => Promise<{customerContextId: string}>;
triggerAuthenticationFlow: (customerContextId: string) => Promise<IFastlaneAuthenticatedCustomerResult>
};
FastlanePaymentComponent: (options: IFastlaneComponentOptions) => Promise<IFastlanePaymentComponent>;
FastlaneCardComponent: (options: Omit<IFastlaneComponentOptions, 'shippingAddress'>) => IFastlaneCardComponent;
}
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './paypal';
export * from './props';
export * from './stripeProps';
export * from './variables';
export * from './fastlane';
2 changes: 1 addition & 1 deletion src/types/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ declare global {
}

export interface IInitializeProps {
onAction: IOnAction
onAction: IOnAction;
}

export interface IGetFirstAndLastName {
Expand Down
Loading

0 comments on commit 57e5448

Please sign in to comment.