Skip to content

Commit 9252acd

Browse files
committed
feat(payment): headless wallet button integration service + paypal strategy
1 parent f16c8d5 commit 9252acd

File tree

44 files changed

+1099
-9
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1099
-9
lines changed

packages/core/auto-export.config.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
"inputPath": "packages/*/src/index.ts",
1515
"outputPath": "packages/core/src/generated/checkout-button-strategies.ts",
1616
"memberPattern": "^create.+ButtonStrategy$"
17+
},
18+
{
19+
"inputPath": "packages/*/src/index.ts",
20+
"outputPath": "packages/core/src/generated/checkout-headless-wallet-strategies.ts",
21+
"memberPattern": "^create.+HeadlessWalletStrategy$"
1722
}
1823
]
1924
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { createTimeout } from '@bigcommerce/request-sender';
2+
3+
export { createHeadlessCheckoutWalletInitializer } from '../checkout-buttons';

packages/core/src/checkout-buttons/checkout-button-error-selector.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { InternalCheckoutSelectors } from '../checkout';
22
import { selector } from '../common/selector';
3+
import HeadlessButtonSelectors from '../headless-buttons/headless-button-selectors';
34

45
import CheckoutButtonSelector from './checkout-button-selector';
56
import { CheckoutButtonMethodType } from './strategies';
@@ -11,7 +12,7 @@ export default class CheckoutButtonErrorSelector {
1112
/**
1213
* @internal
1314
*/
14-
constructor(selectors: InternalCheckoutSelectors) {
15+
constructor(selectors: InternalCheckoutSelectors | HeadlessButtonSelectors) {
1516
this._checkoutButton = selectors.checkoutButton;
1617
}
1718

packages/core/src/checkout-buttons/checkout-button-status-selector.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { InternalCheckoutSelectors } from '../checkout';
22
import { selector } from '../common/selector';
3+
import HeadlessButtonSelectors from '../headless-buttons/headless-button-selectors';
34

45
import CheckoutButtonSelector from './checkout-button-selector';
56
import { CheckoutButtonMethodType } from './strategies';
@@ -11,7 +12,7 @@ export default class CheckoutButtonStatusSelector {
1112
/**
1213
* @internal
1314
*/
14-
constructor(selectors: InternalCheckoutSelectors) {
15+
constructor(selectors: InternalCheckoutSelectors | HeadlessButtonSelectors) {
1516
this._checkoutButton = selectors.checkoutButton;
1617
}
1718

packages/core/src/checkout-buttons/create-checkout-button-selectors.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { InternalCheckoutSelectors } from '../checkout';
2+
import HeadlessButtonSelectors from '../headless-buttons/headless-button-selectors';
23

34
import CheckoutButtonErrorSelector from './checkout-button-error-selector';
45
import CheckoutButtonSelectors from './checkout-button-selectors';
56
import CheckoutButtonStatusSelector from './checkout-button-status-selector';
67

78
export default function createCheckoutButtonSelectors(
8-
selectors: InternalCheckoutSelectors,
9+
selectors: InternalCheckoutSelectors | HeadlessButtonSelectors,
910
): CheckoutButtonSelectors {
1011
const errors = new CheckoutButtonErrorSelector(selectors);
1112
const statuses = new CheckoutButtonStatusSelector(selectors);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { createHeadlessWalletButtonIntegrationService } from '@bigcommerce/checkout-sdk/headless-wallet-button-integration';
2+
3+
import * as defaultCheckoutHeadlessWalletStrategyFactories from '../generated/checkout-headless-wallet-strategies';
4+
import { createHeadlessButtonStore } from '../headless-buttons';
5+
6+
import createHeadlessWalletButtonStrategyRegistry from './create-headless-wallet-button-strategy-registry';
7+
import HeadlessCheckoutWalletInitializer from './headless-checkout-wallet-initializer';
8+
import HeadlessCheckoutWalletStrategyActionCreator from './headless-checkout-wallet-strategy-action-creator';
9+
10+
export default function createHeadlessCheckoutWalletInitializer(): HeadlessCheckoutWalletInitializer {
11+
const store = createHeadlessButtonStore();
12+
13+
const headlessWalletButtonIntegrationService = createHeadlessWalletButtonIntegrationService();
14+
const registryV2 = createHeadlessWalletButtonStrategyRegistry(
15+
headlessWalletButtonIntegrationService,
16+
defaultCheckoutHeadlessWalletStrategyFactories,
17+
);
18+
19+
return new HeadlessCheckoutWalletInitializer(
20+
store,
21+
new HeadlessCheckoutWalletStrategyActionCreator(registryV2),
22+
);
23+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {
2+
DefaultHeadlessWalletButtonIntegrationService,
3+
HeadlessWalletButtonStrategyFactory,
4+
} from '@bigcommerce/checkout-sdk/headless-wallet-button-integration';
5+
import {
6+
CheckoutButtonStrategy,
7+
CheckoutButtonStrategyResolveId,
8+
isResolvableModule,
9+
} from '@bigcommerce/checkout-sdk/payment-integration-api';
10+
11+
import { ResolveIdRegistry } from '../common/registry';
12+
13+
export interface CheckoutHeadlessButtonStrategyFactories {
14+
[key: string]: HeadlessWalletButtonStrategyFactory<CheckoutButtonStrategy>;
15+
}
16+
17+
export default function createHeadlessWalletButtonStrategyRegistry(
18+
headlessIntegrationService: DefaultHeadlessWalletButtonIntegrationService,
19+
checkoutHeadlessButtonStrategyFactories: CheckoutHeadlessButtonStrategyFactories,
20+
): ResolveIdRegistry<CheckoutButtonStrategy, CheckoutButtonStrategyResolveId> {
21+
const registry = new ResolveIdRegistry<
22+
CheckoutButtonStrategy,
23+
CheckoutButtonStrategyResolveId
24+
>();
25+
26+
for (const [, createCheckoutButtonStrategy] of Object.entries(
27+
checkoutHeadlessButtonStrategyFactories,
28+
)) {
29+
if (
30+
!isResolvableModule<
31+
HeadlessWalletButtonStrategyFactory<CheckoutButtonStrategy>,
32+
CheckoutButtonStrategyResolveId
33+
>(createCheckoutButtonStrategy)
34+
) {
35+
continue;
36+
}
37+
38+
for (const resolverId of createCheckoutButtonStrategy.resolveIds) {
39+
registry.register(resolverId, () =>
40+
createCheckoutButtonStrategy(headlessIntegrationService),
41+
);
42+
}
43+
}
44+
45+
return registry;
46+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { bindDecorator as bind } from '@bigcommerce/checkout-sdk/utility';
2+
3+
import { isElementId, setUniqueElementId } from '../common/dom';
4+
import { HeadlessButtonStore } from '../headless-buttons';
5+
import HeadlessButtonSelectors from '../headless-buttons/headless-button-selectors';
6+
7+
import { CheckoutButtonInitializeOptions, CheckoutButtonOptions } from './checkout-button-options';
8+
import CheckoutButtonSelectors from './checkout-button-selectors';
9+
import createCheckoutButtonSelectors from './create-checkout-button-selectors';
10+
import HeadlessCheckoutWalletStrategyActionCreator from './headless-checkout-wallet-strategy-action-creator';
11+
12+
@bind
13+
export default class HeadlessCheckoutWalletInitializer {
14+
private _state: CheckoutButtonSelectors;
15+
16+
constructor(
17+
private _store: HeadlessButtonStore,
18+
private _headlessCheckoutWalletStrategyActionCreator: HeadlessCheckoutWalletStrategyActionCreator,
19+
) {
20+
this._state = createCheckoutButtonSelectors(this._store.getState());
21+
22+
this._store.subscribe((state) => {
23+
this._state = createCheckoutButtonSelectors(state);
24+
});
25+
}
26+
27+
getState(): CheckoutButtonSelectors {
28+
return this._state;
29+
}
30+
31+
subscribe(
32+
subscriber: (state: CheckoutButtonSelectors) => void,
33+
...filters: Array<(state: CheckoutButtonSelectors) => any>
34+
): () => void {
35+
return this._store.subscribe(
36+
() => subscriber(this.getState()),
37+
(state) => state.checkoutButton.getState(),
38+
...filters.map(
39+
(filter) => (state: HeadlessButtonSelectors) =>
40+
filter(createCheckoutButtonSelectors(state)),
41+
),
42+
);
43+
}
44+
45+
initializeHeadlessButton(
46+
options: CheckoutButtonInitializeOptions,
47+
): Promise<CheckoutButtonSelectors> {
48+
const containerIds = this.getContainerIds(options);
49+
50+
return Promise.all(
51+
containerIds.map((containerId) => {
52+
const action = this._headlessCheckoutWalletStrategyActionCreator.initialize({
53+
...options,
54+
containerId,
55+
});
56+
const queueId = `checkoutHeadlessButtonStrategy:${options.methodId}:${containerId}`;
57+
58+
return this._store.dispatch(action, { queueId });
59+
}),
60+
).then(() => this.getState());
61+
}
62+
63+
deinitializeHeadlessButton(options: CheckoutButtonOptions): Promise<CheckoutButtonSelectors> {
64+
const action = this._headlessCheckoutWalletStrategyActionCreator.deinitialize(options);
65+
const queueId = `checkoutHeadlessButtonStrategy:${options.methodId}`;
66+
67+
return this._store.dispatch(action, { queueId }).then(() => this.getState());
68+
}
69+
70+
private getContainerIds(options: CheckoutButtonInitializeOptions) {
71+
return isElementId(options.containerId)
72+
? [options.containerId]
73+
: setUniqueElementId(options.containerId, `${options.methodId}-container`);
74+
}
75+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { createAction, ThunkAction } from '@bigcommerce/data-store';
2+
import { concat, defer, empty, of } from 'rxjs';
3+
import { catchError } from 'rxjs/operators';
4+
5+
import { throwErrorAction } from '../common/error';
6+
import HeadlessButtonSelectors from '../headless-buttons/headless-button-selectors';
7+
8+
import {
9+
CheckoutButtonActionType,
10+
DeinitializeButtonAction,
11+
InitializeButtonAction,
12+
} from './checkout-button-actions';
13+
import { CheckoutButtonInitializeOptions, CheckoutButtonOptions } from './checkout-button-options';
14+
import CheckoutButtonRegistryV2 from './checkout-button-strategy-registry-v2';
15+
16+
export default class HeadlessCheckoutWalletStrategyActionCreator {
17+
constructor(private _registryV2: CheckoutButtonRegistryV2) {}
18+
19+
initialize(
20+
options: CheckoutButtonInitializeOptions,
21+
): ThunkAction<InitializeButtonAction, HeadlessButtonSelectors> {
22+
return (store) => {
23+
const meta = {
24+
methodId: options.methodId,
25+
containerId: options.containerId,
26+
};
27+
28+
if (
29+
store.getState().checkoutButton.isInitialized(options.methodId, options.containerId)
30+
) {
31+
return empty();
32+
}
33+
34+
return concat(
35+
of(
36+
createAction(
37+
CheckoutButtonActionType.InitializeButtonRequested,
38+
undefined,
39+
meta,
40+
),
41+
),
42+
defer(() =>
43+
this._registryV2
44+
.get({ id: options.methodId })
45+
.initialize(options)
46+
.then(() =>
47+
createAction(
48+
CheckoutButtonActionType.InitializeButtonSucceeded,
49+
undefined,
50+
meta,
51+
),
52+
),
53+
),
54+
).pipe(
55+
catchError((error) =>
56+
throwErrorAction(CheckoutButtonActionType.InitializeButtonFailed, error, meta),
57+
),
58+
);
59+
};
60+
}
61+
62+
deinitialize(
63+
options: CheckoutButtonOptions,
64+
): ThunkAction<DeinitializeButtonAction, HeadlessButtonSelectors> {
65+
return (store) => {
66+
const meta = { methodId: options.methodId };
67+
68+
if (!store.getState().checkoutButton.isInitialized(options.methodId)) {
69+
return empty();
70+
}
71+
72+
return concat(
73+
of(
74+
createAction(
75+
CheckoutButtonActionType.DeinitializeButtonRequested,
76+
undefined,
77+
meta,
78+
),
79+
),
80+
defer(() =>
81+
this._registryV2
82+
.get({ id: options.methodId })
83+
.deinitialize()
84+
.then(() =>
85+
createAction(
86+
CheckoutButtonActionType.DeinitializeButtonSucceeded,
87+
undefined,
88+
meta,
89+
),
90+
),
91+
),
92+
).pipe(
93+
catchError((error) =>
94+
throwErrorAction(
95+
CheckoutButtonActionType.DeinitializeButtonFailed,
96+
error,
97+
meta,
98+
),
99+
),
100+
);
101+
};
102+
}
103+
}

packages/core/src/checkout-buttons/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { default as createCheckoutButtonInitializer } from './create-checkout-button-initializer';
2+
export { default as createHeadlessCheckoutWalletInitializer } from './create-headless-checkout-wallet-initializer';
23
export { default as checkoutButtonReducer } from './checkout-button-reducer';
34
export {
45
default as CheckoutButtonSelector,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { createCheckoutButtonSelectorFactory } from '../checkout-buttons';
2+
3+
import HeadlessButtonSelectors from './headless-button-selectors';
4+
5+
import { HeadlessButtonStoreState } from './';
6+
7+
export type HeadlessButtonSelectorsFactory = (
8+
state: HeadlessButtonStoreState,
9+
) => HeadlessButtonSelectors;
10+
11+
export function createHeadlessButtonSelectorsFactory(): HeadlessButtonSelectorsFactory {
12+
const createCheckoutButtonSelector = createCheckoutButtonSelectorFactory();
13+
14+
return (state) => {
15+
const checkoutButton = createCheckoutButtonSelector(state.checkoutButton);
16+
17+
return {
18+
checkoutButton,
19+
};
20+
};
21+
}
22+
23+
export default function createHeadlessButtonSelectors(
24+
state: HeadlessButtonStoreState,
25+
): HeadlessButtonSelectors {
26+
return createHeadlessButtonSelectorsFactory()(state);
27+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Action, combineReducers, Reducer } from '@bigcommerce/data-store';
2+
3+
import { checkoutButtonReducer } from '../checkout-buttons';
4+
5+
import HeadlessButtonStoreState from './headless-button-store-state';
6+
7+
export default function createHeadlessButtonStoreReducer(): Reducer<
8+
HeadlessButtonStoreState,
9+
Action
10+
> {
11+
return combineReducers({
12+
checkoutButton: checkoutButtonReducer,
13+
});
14+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { createDataStore } from '@bigcommerce/data-store';
2+
3+
import { createHeadlessButtonSelectorsFactory } from './create-haedless-button-selectors';
4+
import createHeadlessButtonStoreReducer from './create-headless-button-store-reducer';
5+
import HeadlessButtonStore from './headless-button-store';
6+
import HeadlessButtonStoreState from './headless-button-store-state';
7+
8+
export default function createHeadlessButtonStore(
9+
initialState: Partial<HeadlessButtonStoreState> = {},
10+
): HeadlessButtonStore {
11+
const createHeadlessButtonSelectors = createHeadlessButtonSelectorsFactory();
12+
const stateTransformer = (state: HeadlessButtonStoreState) =>
13+
createHeadlessButtonSelectors(state);
14+
15+
return createDataStore(createHeadlessButtonStoreReducer(), initialState, {
16+
stateTransformer,
17+
});
18+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { CheckoutButtonSelector } from '../checkout-buttons';
2+
3+
export default interface HeadlessButtonSelectors {
4+
checkoutButton: CheckoutButtonSelector;
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { CheckoutButtonState } from '../checkout-buttons';
2+
3+
export default interface HeadlessButtonStoreState {
4+
checkoutButton: CheckoutButtonState;
5+
}

0 commit comments

Comments
 (0)