Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
import { createSagePayPaymentStrategy } from '@bigcommerce/checkout-sdk/integrations/sagepay';
import React, { type FunctionComponent, useCallback } from 'react';

import { useCheckout } from '@bigcommerce/checkout/contexts';

import { isExperimentEnabled } from '../../common/utility';
import {
withHostedCreditCardFieldset,
type WithInjectedHostedCreditCardFieldsetProps,
Expand Down Expand Up @@ -35,6 +38,13 @@ const HostedCreditCardPaymentMethod: FunctionComponent<
initializePayment,
...rest
}) => {
const { selectedState: config } = useCheckout(({ data }) => data.getConfig());
const isCBAMPGSResolverEnabled = isExperimentEnabled(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

config?.checkoutSettings,
'PI-4748.cba_resolver_configuration',
false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose of this fallback value is for the time when PI-4748.cba_resolver_configuration is undefined in the checkout settings.
If we have surfaced it, we don't need to explicitly set the value here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we have default value true and we need false
image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please explain when the experiment will not appear in the features[]?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before the experiment is surfaced/registered in LaunchDarkly (or whatever feature flag service is used), the key won't exist in features[] at all — features['PI-4748.cba_resolver_configuration'] will be undefined.

In that case ?? fallbackValue kicks in. Since the default fallback in isExperimentEnabled is true, without explicitly passing false here, cba_mpgs would resolve through the new flow on day one — before we've intentionally enabled it for anyone. That's exactly what we want to avoid.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, this is intended use case when we introduced this fallback value.

The experiment appears to be added on Jun 23, 2026 at https://app.launchdarkly.com/projects/default/flags/PI-4748.cba_resolver_configuration/targeting?env=production&selected-env=production.

Is anything blocking us from showing it to Checkout FE now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I turned on this flag if you about that. Or I miss understood

);

const initializeHostedCreditCardPayment: CreditCardPaymentMethodProps['initializePayment'] =
useCallback(
async (options, selectedInstrument) => {
Expand All @@ -46,14 +56,14 @@ const HostedCreditCardPaymentMethod: FunctionComponent<
createCyberSourcePaymentStrategy,
createCyberSourceV2PaymentStrategy,
createSagePayPaymentStrategy,
createCBAMPGSPaymentStrategy,
...(!isCBAMPGSResolverEnabled ? [createCBAMPGSPaymentStrategy] : []),
],
creditCard: getHostedFormOptions && {
form: await getHostedFormOptions(selectedInstrument),
},
});
},
[getHostedFormOptions, initializePayment],
[getHostedFormOptions, initializePayment, isCBAMPGSResolverEnabled],
);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,15 @@ const PaymentMethodContainer: ComponentType<
setValidationSchema,
};

const ResolvedPaymentMethod = resolvePaymentMethod({
id: method.id,
gateway: method.gateway,
type: method.type,
});
const config = checkoutState.data.getConfig();
const ResolvedPaymentMethod = resolvePaymentMethod(
{
id: method.id,
gateway: method.gateway,
type: method.type,
},
config?.checkoutSettings,
);

if (!ResolvedPaymentMethod) {
return (
Expand Down
26 changes: 24 additions & 2 deletions packages/core/src/app/payment/resolvePaymentMethod.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
import type { CheckoutSettings } from '@bigcommerce/checkout-sdk';
import { type ComponentType } from 'react';

import {
type PaymentMethodProps,
type PaymentMethodResolveId,
} from '@bigcommerce/checkout/payment-integration-api';

import { isExperimentEnabled } from '../common/utility';
import { resolveLazyComponent } from '../common/resolver';
import * as lazyPaymentMethods from '../generated/paymentIntegrations';

export default function resolvePaymentMethod(
query: PaymentMethodResolveId,
checkoutSettings?: CheckoutSettings,
): ComponentType<PaymentMethodProps> | undefined {
const { ComponentRegistry, ...allExports } = lazyPaymentMethods;
const filteredMethods = Object.fromEntries(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we adding this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @animesh1987 we want to experiments for our PaymentMethods
However we can't somehow change this
image
that's why one place where we can check this experiments in this file, and we filter our payment methods if experiment is false then this method should not be in result arr

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for behavior of this PR we set experiment name in config then in file resolvePaymentMethod we check is this experiment true or false if it is true pr if we don't have experiment then this method will be in result arr if not than we will not have this mthod in arr

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attaching an experiment name to a payment method config requires some explanations on the purpose of this experiment.

Could we use the experiment to control payment method identifiers instead of making it part of the payment method resolution logic?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This experiment is specifically needed to safely migrate the CBA MPGS and other payment providers that are using hosted fields from the legacy core flow to the new resolver-based package architecture.
Why it needs to be on the resolver level, not on the identifier level:
The resolver is the exact point where the routing decision is made — it determines whether a payment method renders via the new package component or falls back to the legacy V1 core component. Controlling the identifier itself wouldn't give us that routing control, since the same cba_mpgs ID needs to exist in both paths during the transition.
Why we're being cautious:
We previously had an incident with the hosted fields migration process, so we want to roll this out gradually and have a clean rollback path. The experiment flag gives us that safety net.
What happens after the migration is complete:
Once we've validated the rollout, we'll remove both the experiment flag and all the conditional logic around it — it's intentionally duplicated in the code to make that cleanup straightforward (each side clearly shows what gets removed).
Future migrations:
We'll need similar experiments for migrating other payment providers through the same pattern. The experiment field on PaymentMethodResolveId is designed to be generic for exactly that purpose — any future provider migration can use the same mechanism without changes to the resolver infrastructure itself.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My question is narrower: it's about where the gate lives, not whether it exists.

Agreed on the rollout safety, the experiment, gradual rollout, and clean revert path. All make sense given the prior incident, no objection there.

The point that the routing decision must happen at runtime in the resolver is right (the registry is generated at build time). But that doesn't require an experiment to be a field on PaymentMethodResolveId.

If we do want a generic per-package migration-gate mechanism long term, I think that's worth its own small design rather than an experiment field.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 this is not long term, we need this for migration maybe 3 providers.
 Maybe you have some suggestions how we can resolve it ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the context. That changes the perspective.

this is not long term, we need this for migration maybe 3 providers.

If possible, could you please add this context as comments? My concern is we will soon forget about it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created task for remove this functional after migration will be completed PI-5464

Object.entries(ComponentRegistry).map(([key, value]) => {
const methods = value.filter((resolveId) => {
const { experiment } = resolveId as PaymentMethodResolveId;

if (!experiment) {
return true;
}

// should be normalized after config
const normalizedKey = experiment.replace('_', '.');

return isExperimentEnabled(checkoutSettings, normalizedKey, false);
});

return [key, methods];
}),
);

const components = Object.fromEntries(
Object.keys(ComponentRegistry).map((key) => [
Object.keys(filteredMethods).map((key) => [
Comment thread
cursor[bot] marked this conversation as resolved.
key,
allExports[key as keyof typeof allExports],
]),
Expand All @@ -22,6 +44,6 @@ export default function resolvePaymentMethod(
return resolveLazyComponent<PaymentMethodResolveId, PaymentMethodProps>(
query,
components,
ComponentRegistry,
filteredMethods,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const HostedCreditCardPaymentMethod: FunctionComponent<PaymentMethodProps> = ({

export default toResolvableComponent<PaymentMethodProps, PaymentMethodResolveId>(
HostedCreditCardPaymentMethod,
// experiment should be write with underscore because point after build will set this value like variable
[
{
id: 'hosted-credit-card',
Expand All @@ -38,5 +39,6 @@ export default toResolvableComponent<PaymentMethodProps, PaymentMethodResolveId>
{ id: 'credit_card', gateway: 'checkoutcom' },

{ id: 'tdonlinemart' },
{ id: 'cba_mpgs', experiment: 'PI-4748_cba_resolver_configuration' },
],
);
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,15 @@ const getDefaultProps = (overrides = {}): HostedCreditCardComponentProps =>
initializePayment: jest.fn().mockResolvedValue(undefined),
deinitializePayment: jest.fn(),
},
checkoutState: {},
checkoutState: {
data: {
getConfig: jest.fn().mockReturnValue({
checkoutSettings: {
features: {},
},
}),
},
},
paymentForm: {
setFieldTouched: jest.fn(),
setFieldValue: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type LegacyHostedFormOptions,
} from '@bigcommerce/checkout-sdk';
import { createBlueSnapDirectCreditCardPaymentStrategy } from '@bigcommerce/checkout-sdk/integrations/bluesnap-direct';
import { createCBAMPGSPaymentStrategy } from '@bigcommerce/checkout-sdk/integrations/cba-mpgs';
import { createCheckoutComCreditCardPaymentStrategy } from '@bigcommerce/checkout-sdk/integrations/checkoutcom-custom';
import { createCreditCardPaymentStrategy } from '@bigcommerce/checkout-sdk/integrations/credit-card';
import { createTDOnlineMartPaymentStrategy } from '@bigcommerce/checkout-sdk/integrations/td-bank';
Expand Down Expand Up @@ -43,6 +44,11 @@ const HostedCreditCardComponent: FunctionComponent<HostedCreditCardComponentProp
}) => {
const [focusedFieldType, setFocusedFieldType] = useState<string>();

const isCBAMPGSResolverEnabled =
checkoutState.data.getConfig()?.checkoutSettings.features?.[
'PI-4748.cba_resolver_configuration'
] ?? false;

const { setFieldTouched, setFieldValue, setSubmitted, submitForm } = paymentForm;
const isInstrumentCardCodeRequiredProp = isInstrumentCardCodeRequiredSelector(checkoutState);
const isInstrumentCardNumberRequiredProp =
Expand Down Expand Up @@ -257,6 +263,7 @@ const HostedCreditCardComponent: FunctionComponent<HostedCreditCardComponentProp
integrations: [
createCreditCardPaymentStrategy,
createBlueSnapDirectCreditCardPaymentStrategy,
...(isCBAMPGSResolverEnabled ? [createCBAMPGSPaymentStrategy] : []),
createTDOnlineMartPaymentStrategy,
createCheckoutComCreditCardPaymentStrategy,
],
Expand All @@ -268,7 +275,12 @@ const HostedCreditCardComponent: FunctionComponent<HostedCreditCardComponentProp
}),
});
},
[getHostedFormOptions, initializePayment, isHostedFormEnabled],
[
getHostedFormOptions,
initializePayment,
isHostedFormEnabled,
isCBAMPGSResolverEnabled,
],
);

const hostedStoredCardValidationSchema = getHostedInstrumentValidationSchema({ language });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ type PaymentMethodResolveId = RequireAtLeastOne<{
id?: string;
gateway?: string;
type?: string;
experiment?: string;
}>;

export default PaymentMethodResolveId;