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
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import {
Divider
} from '@salesforce/retail-react-app/app/components/shared/ui'
import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast'
import {useShopperBasketsV2Mutation as useShopperBasketsMutation, useCustomerType} from '@salesforce/commerce-sdk-react'
import {

Choose a reason for hiding this comment

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

is this formatting change a lint fix or something? I assume we dont need to mess with one-click in general

Copy link
Author

Choose a reason for hiding this comment

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

Yes, it was just a lint fix. Trying to reduce failing tests.

useShopperBasketsV2Mutation as useShopperBasketsMutation,
useCustomerType
} from '@salesforce/commerce-sdk-react'
import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
import {useCheckoutAutoSelect} from '@salesforce/retail-react-app/app/hooks/use-checkout-auto-select'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-curre
import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast'
import {useCurrency} from '@salesforce/retail-react-app/app/hooks/use-currency'
import {useShopperBasketsV2Mutation as useShopperBasketsMutation, useCustomerType} from '@salesforce/commerce-sdk-react'
import {
useShopperBasketsV2Mutation as useShopperBasketsMutation,
useCustomerType
} from '@salesforce/commerce-sdk-react'
import {useCheckout} from '@salesforce/retail-react-app/app/pages/checkout-one-click/util/checkout-context'
import Payment from '@salesforce/retail-react-app/app/pages/checkout-one-click/partials/one-click-payment'
import {CurrencyProvider} from '@salesforce/retail-react-app/app/contexts'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import StoreDisplay from '@salesforce/retail-react-app/app/components/store-disp
// Hooks
import {useCheckout} from '@salesforce/retail-react-app/app/pages/checkout-one-click/util/checkout-context'
import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
import {useShopperBasketsV2Mutation as useShopperBasketsMutation, useStores} from '@salesforce/commerce-sdk-react'
import {
useShopperBasketsV2Mutation as useShopperBasketsMutation,
useStores
} from '@salesforce/commerce-sdk-react'
import {isPickupShipment} from '@salesforce/retail-react-app/app/utils/shipment-utils'

const PickupAddress = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,17 +295,11 @@ const SFPaymentsSheet = forwardRef((props, ref) => {
?.clientSecret
}

// Read setup_future_usage from backend response, fallback to manual calculation if not available
// TODO: The fallback is temporary that's to be removed in next iteration.
const setupFutureUsage =
orderPaymentInstrument?.paymentReference?.gatewayProperties?.stripe
?.setup_future_usage
const orderStripeGatewayProperties =
orderPaymentInstrument?.paymentReference?.gatewayProperties?.stripe || {}
const setupFutureUsage = orderStripeGatewayProperties?.setupFutureUsage
if (setupFutureUsage) {
paymentIntent.setup_future_usage = setupFutureUsage
} else if (futureUsageOffSession) {
paymentIntent.setup_future_usage = 'off_session'
} else if (shouldSavePaymentMethod) {
paymentIntent.setup_future_usage = 'on_session'
}

// Update the redirect return URL to include the related order no
Expand Down Expand Up @@ -595,6 +589,8 @@ const SFPaymentsSheet = forwardRef((props, ref) => {
checkoutComponent.current?.destroy()
checkoutComponent.current = null
}
// Omit savedPaymentMethodsKey: init once with current SPM; re-initing when SPM list changes
// causes Stripe/Adyen to complain.
}, [
isCustomerDataLoading,
sfp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1745,24 +1745,17 @@ describe('SFPaymentsSheet', () => {

describe('lifecycle', () => {
test('cleans up checkout component on unmount', () => {
/**checkout effect often never runs (ref not attached in time)
* The assertion is: destroy is called exactly as many times as checkout was created.
expect(mockCheckoutDestroy).toHaveBeenCalledTimes(mockCheckout.mock.calls.length)
So when checkout is never created (0 calls), we expect 0 destroy calls; when it is created once, we expect destroy once. The test no longer depends on checkout being created in this env. */
const ref = React.createRef()
const {unmount} = renderWithCheckoutContext(
<SFPaymentsSheet
ref={ref}
ref={mockRef}
onCreateOrder={mockOnCreateOrder}
onError={mockOnError}
/>
)

unmount()

// When checkout was created, destroy must be called on unmount (cleanup).
// When ref/effect never run in test env, neither checkout nor destroy are called.
expect(mockCheckoutDestroy).toHaveBeenCalledTimes(mockCheckout.mock.calls.length)
expect(mockCheckoutDestroy).toHaveBeenCalled()
})
})

Expand Down Expand Up @@ -1806,10 +1799,9 @@ So when checkout is never created (0 calls), we expect 0 destroy calls; when it
isLoading: false
}))

const ref = React.createRef()
const {rerender} = renderWithCheckoutContext(
<SFPaymentsSheet
ref={ref}
ref={mockRef}
onCreateOrder={mockOnCreateOrder}
onError={mockOnError}
/>
Expand All @@ -1819,10 +1811,20 @@ So when checkout is never created (0 calls), we expect 0 destroy calls; when it
expect(screen.getByTestId('toggle-card')).toBeInTheDocument()
})

await waitFor(
() => {
expect(mockUpdateAmount).toHaveBeenCalledWith(100.0)
},
{timeout: 2000}
)

mockUpdateAmount.mockClear()

const updatedBasket = {
...initialBasket,
orderTotal: 150.0
}

mockUseCurrentBasket.mockImplementation(() => ({
data: updatedBasket,
derivedData: {
Expand All @@ -1838,22 +1840,19 @@ So when checkout is never created (0 calls), we expect 0 destroy calls; when it
rerender(
<CheckoutProvider>
<SFPaymentsSheet
ref={ref}
ref={mockRef}
onCreateOrder={mockOnCreateOrder}
onError={mockOnError}
/>
</CheckoutProvider>
)

await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 2500))
})

// When checkout was created, updateAmount is called with initial then updated orderTotal
const hadCheckout = mockCheckout.mock.calls.length > 0
const hadUpdate100 = mockUpdateAmount.mock.calls.some((call) => call[0] === 100.0)
const hadUpdate150 = mockUpdateAmount.mock.calls.some((call) => call[0] === 150.0)
expect(!hadCheckout || (hadUpdate100 && hadUpdate150)).toBe(true)
await waitFor(
() => {
expect(mockUpdateAmount).toHaveBeenCalledWith(150.0)
},
{timeout: 2000}
)
})

test('does not call updateAmount when orderTotal is undefined', async () => {
Expand Down Expand Up @@ -1911,20 +1910,18 @@ So when checkout is never created (0 calls), we expect 0 destroy calls; when it

renderWithCheckoutContext(
<SFPaymentsSheet
ref={React.createRef()}
ref={mockRef}
onCreateOrder={mockOnCreateOrder}
onError={mockOnError}
/>
)

await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 2500))
})

// When checkout was created, updateAmount is called with orderTotal on initial render
const hadCheckout = mockCheckout.mock.calls.length > 0
const hadUpdate250_75 = mockUpdateAmount.mock.calls.some((call) => call[0] === 250.75)
expect(!hadCheckout || hadUpdate250_75).toBe(true)
await waitFor(
() => {
expect(mockUpdateAmount).toHaveBeenCalledWith(250.75)
},
{timeout: 2000}
)
})
})
})
33 changes: 10 additions & 23 deletions packages/template-retail-react-app/app/utils/sf-payments-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,21 +199,6 @@ export const getGatewayFromPaymentMethod = (
return null
}

/**
* Determines the setup_future_usage value for Stripe payment intents based on configuration and user preference.
* @param {boolean} storePaymentMethod - Whether the user wants to save the payment method
* @param {boolean} futureUsageOffSession - Whether off-session future usage is enabled in configuration
* @returns {string|null} 'on_session', 'off_session', or null
*/
export const getSetupFutureUsage = (storePaymentMethod, futureUsageOffSession) => {
if (futureUsageOffSession) {
return SETUP_FUTURE_USAGE.OFF_SESSION
} else if (storePaymentMethod) {
return SETUP_FUTURE_USAGE.ON_SESSION
}
return null
}

/**
* Creates a payment instrument body for Salesforce Payments (for basket or order).
* @param {Object} params - Parameters for creating payment instrument body
Expand Down Expand Up @@ -265,14 +250,16 @@ export const createPaymentInstrumentBody = ({
}
}

if (!isPostRequest && gateway === PAYMENT_GATEWAYS.STRIPE && storePaymentMethod) {
const setupFutureUsage = getSetupFutureUsage(storePaymentMethod, futureUsageOffSession)
if (setupFutureUsage) {
paymentReferenceRequest.gateway = PAYMENT_GATEWAYS.STRIPE
paymentReferenceRequest.gatewayProperties = {
stripe: {
setupFutureUsage
}
if (!isPostRequest && gateway === PAYMENT_GATEWAYS.STRIPE) {
const setupFutureUsage = storePaymentMethod
? futureUsageOffSession
? SETUP_FUTURE_USAGE.OFF_SESSION
: SETUP_FUTURE_USAGE.ON_SESSION
: null
paymentReferenceRequest.gateway = PAYMENT_GATEWAYS.STRIPE
paymentReferenceRequest.gatewayProperties = {
stripe: {
...(setupFutureUsage && {setupFutureUsage})
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
findPaymentAccount,
createPaymentInstrumentBody,
getGatewayFromPaymentMethod,
getSetupFutureUsage,
transformPaymentMethodReferences,
getExpressPaymentMethodType
} from '@salesforce/retail-react-app/app/utils/sf-payments-utils'
Expand Down Expand Up @@ -1301,7 +1300,7 @@ describe('sf-payments-utils', () => {
})
})

test('does not include gatewayProperties when storePaymentMethod is false and futureUsageOffSession is false', () => {
test('includes Stripe gateway with empty stripe props when storePaymentMethod is false (no setupFutureUsage)', () => {
const paymentMethods = [{paymentMethodType: 'card', accountId: 'acct_123'}]
const paymentMethodSetAccounts = [{vendor: 'Stripe', accountId: 'acct_123'}]
const result = createPaymentInstrumentBody({
Expand All @@ -1315,8 +1314,8 @@ describe('sf-payments-utils', () => {
paymentMethodSetAccounts
})

expect(result.paymentReferenceRequest.gateway).toBeUndefined()
expect(result.paymentReferenceRequest.gatewayProperties).toBeUndefined()
expect(result.paymentReferenceRequest.gateway).toBe('stripe')
expect(result.paymentReferenceRequest.gatewayProperties.stripe).toEqual({})
})

test('does not include shippingPreference when null', () => {
Expand Down Expand Up @@ -1549,32 +1548,6 @@ describe('sf-payments-utils', () => {
})
})

describe('getSetupFutureUsage', () => {
test('returns off_session when futureUsageOffSession is true', () => {
const result = getSetupFutureUsage(false, true)

expect(result).toBe('off_session')
})

test('returns on_session when storePaymentMethod is true and futureUsageOffSession is false', () => {
const result = getSetupFutureUsage(true, false)

expect(result).toBe('on_session')
})

test('returns null when both are false', () => {
const result = getSetupFutureUsage(false, false)

expect(result).toBeNull()
})

test('returns off_session when both are true (futureUsageOffSession takes precedence)', () => {
const result = getSetupFutureUsage(true, true)

expect(result).toBe('off_session')
})
})

describe('transformPaymentMethodReferences', () => {
test('returns empty array when customer is null', () => {
const result = transformPaymentMethodReferences(null, {})
Expand Down Expand Up @@ -1954,10 +1927,8 @@ describe('sf-payments-utils', () => {

describe('getExpressPaymentMethodType', () => {
test('returns card for googlepay with Stripe gateway', () => {
const paymentMethods = [
{paymentMethodType: 'googlepay', accountId: 'stripe_express_acct'}
]
const paymentMethodSetAccounts = [{vendor: 'Stripe', accountId: 'stripe_express_acct'}]
const paymentMethods = [{paymentMethodType: 'googlepay', accountId: 'acct_123'}]
const paymentMethodSetAccounts = [{vendor: 'Stripe', accountId: 'acct_123'}]
const result = getExpressPaymentMethodType(
'googlepay',
paymentMethods,
Expand All @@ -1967,10 +1938,8 @@ describe('sf-payments-utils', () => {
})

test('returns googlepay for googlepay with Adyen gateway', () => {
const paymentMethods = [
{paymentMethodType: 'googlepay', accountId: 'adyen_express_acct'}
]
const paymentMethodSetAccounts = [{vendor: 'Adyen', accountId: 'adyen_express_acct'}]
const paymentMethods = [{paymentMethodType: 'googlepay', accountId: 'adyen_acct'}]
const paymentMethodSetAccounts = [{vendor: 'Adyen', accountId: 'adyen_acct'}]
const result = getExpressPaymentMethodType(
'googlepay',
paymentMethods,
Expand All @@ -1980,10 +1949,8 @@ describe('sf-payments-utils', () => {
})

test('returns card for applepay with Stripe gateway', () => {
const paymentMethods = [
{paymentMethodType: 'applepay', accountId: 'stripe_express_acct'}
]
const paymentMethodSetAccounts = [{vendor: 'Stripe', accountId: 'stripe_express_acct'}]
const paymentMethods = [{paymentMethodType: 'applepay', accountId: 'acct_123'}]
const paymentMethodSetAccounts = [{vendor: 'Stripe', accountId: 'acct_123'}]
const result = getExpressPaymentMethodType(
'applepay',
paymentMethods,
Expand All @@ -1993,8 +1960,8 @@ describe('sf-payments-utils', () => {
})

test('returns type unchanged for non-mapped types', () => {
const paymentMethods = [{paymentMethodType: 'paypal', accountId: 'stripe_acct'}]
const paymentMethodSetAccounts = [{vendor: 'Stripe', accountId: 'stripe_acct'}]
const paymentMethods = [{paymentMethodType: 'paypal', accountId: 'acct_123'}]
const paymentMethodSetAccounts = [{vendor: 'Stripe', accountId: 'acct_123'}]
const result = getExpressPaymentMethodType(
'paypal',
paymentMethods,
Expand Down
Loading