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 @@ -9,12 +9,16 @@ import {useCustomer, useCustomerId, useCustomerType} from '@salesforce/commerce-

/**
* A hook that returns the current customer.
*
* @param {Array<string>} [expand] - Optional array of fields to expand in the customer query
*/
export const useCurrentCustomer = () => {
export const useCurrentCustomer = (expand) => {
const customerId = useCustomerId()
const {isRegistered, isGuest, customerType} = useCustomerType()
const query = useCustomer({parameters: {customerId}}, {enabled: !!customerId && isRegistered})
const parameters = {
customerId,
...(expand && {expand})
}
const query = useCustomer({parameters}, {enabled: !!customerId && isRegistered})
const value = {
...query,
data: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import {
buildTheme,
getSFPaymentsInstrument,
createPaymentInstrumentBody,
getClientSecret
transformPaymentMethodReferences
} from '@salesforce/retail-react-app/app/utils/sf-payments-utils'
import logger from '@salesforce/retail-react-app/app/utils/logger-instance'

Expand All @@ -60,7 +60,7 @@ const SFPaymentsSheet = forwardRef((props, ref) => {
const navigate = useNavigation()

const {data: basket} = useCurrentBasket()
const {data: customer} = useCurrentCustomer()
const {data: customer} = useCurrentCustomer(['paymentmethodreferences'])

const isPickupOnly =
basket?.shipments?.length > 0 &&
Expand Down Expand Up @@ -358,11 +358,20 @@ const SFPaymentsSheet = forwardRef((props, ref) => {

// Track created payment intent
const paymentIntent = {
client_secret: getClientSecret(orderPaymentInstrument),
client_secret:
orderPaymentInstrument?.paymentReference?.gatewayProperties?.stripe
?.clientSecret,
id: orderPaymentInstrument.paymentReference.paymentReferenceId
}

if (futureUsageOffSession) {
// 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
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'
Expand Down Expand Up @@ -405,22 +414,9 @@ const SFPaymentsSheet = forwardRef((props, ref) => {
// Update the redirect return URL to include the related order no
config.current.options.returnUrl += '?orderNo=' + updatedOrder.orderNo

// Update Elements to match Payment Intent's setup_future_usage before SDK confirms
const paymentIntentFunction = async () => {
const selectedPaymentMethod = checkoutComponent.current?.selectedPaymentMethod
if (selectedPaymentMethod?.asSavedPaymentMethodComponent) {
const spmComponent = selectedPaymentMethod.asSavedPaymentMethodComponent()
if (spmComponent) {
spmComponent.setSavePaymentMethodFuture(futureUsageOffSession)
}
}

return paymentIntent
}

// Confirm the payment
const result = await checkoutComponent.current.confirm(
paymentIntentFunction,
async () => paymentIntent,
billingDetails,
shippingDetails
)
Expand Down Expand Up @@ -486,11 +482,23 @@ const SFPaymentsSheet = forwardRef((props, ref) => {
confirmPayment
}))

const savedPaymentMethods = useMemo(
() => transformPaymentMethodReferences(customer, paymentConfig),
[customer, paymentConfig]
)

useEffect(() => {
if (sfp && metadata && containerElementRef.current && paymentConfig) {
const paymentMethodSetAccounts = (paymentConfig.paymentMethodSetAccounts || []).map(
(account) => ({
...account,
gatewayId: account.accountId
})
)

const paymentMethodSet = {
paymentMethods: paymentConfig.paymentMethods,
paymentMethodSetAccounts: paymentConfig.paymentMethodSetAccounts
paymentMethodSetAccounts: paymentMethodSetAccounts
}

config.current = {
Expand All @@ -506,7 +514,8 @@ const SFPaymentsSheet = forwardRef((props, ref) => {
customer?.isRegistered && customer?.customerId
),
// Suppress "Make payment method default" checkbox since we don't support default SPM yet
showSaveAsDefaultCheckbox: false
showSaveAsDefaultCheckbox: false,
savedPaymentMethods: savedPaymentMethods
}
}

Expand Down Expand Up @@ -549,7 +558,9 @@ const SFPaymentsSheet = forwardRef((props, ref) => {
containerElementRef.current,
paymentConfig,
cardCaptureAutomatic,
customer?.isRegistered
customer?.isRegistered,
customer?.customerId,
savedPaymentMethods
])

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,14 @@ jest.mock('@salesforce/commerce-sdk-react', () => {
defaultShippingMethodId: 'DefaultShippingMethod'
},
refetch: mockRefetchShippingMethods
})
}),
useCustomerId: () => 'customer123',
useCustomerType: () => ({
isRegistered: true,
isGuest: false,
customerType: 'registered'
}),
useCustomer: jest.fn()
}
})

Expand Down Expand Up @@ -139,15 +146,21 @@ jest.mock('@salesforce/retail-react-app/app/hooks/use-current-basket', () => ({
useCurrentBasket: () => mockUseCurrentBasket()
}))

jest.mock('@salesforce/retail-react-app/app/hooks/use-current-customer', () => ({
useCurrentCustomer: () => ({
data: {
customerId: 'customer123',
isGuest: false,
isRegistered: true,
email: 'test@example.com'
}
})
const mockCustomer = {
customerId: 'customer123',
isGuest: false,
isRegistered: true,
email: 'test@example.com',
paymentMethodReferences: []
}

// Get the mocked useCustomer from commerce-sdk-react
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mockUseCustomer = require('@salesforce/commerce-sdk-react').useCustomer

Choose a reason for hiding this comment

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

Is using require like this an established pattern in PWA?

Copy link
Author

Choose a reason for hiding this comment

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

There are a few test modules (shipping-address.test.js, use-add-to-cart-modal.test.js) in PWA that use it.

Choose a reason for hiding this comment

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

Feels suspect having to disable lint rule here. It'd be good to at least try to find another solution here in case this doesn't jive well with the PWA reviewers when we merge to develop. Otherwise PR is looking good and shaping up!


// Set default implementation
mockUseCustomer.mockImplementation(() => ({
data: mockCustomer
}))

jest.mock('@salesforce/retail-react-app/app/hooks/use-einstein', () => {
Expand Down Expand Up @@ -995,6 +1008,187 @@ describe('SFPaymentsSheet', () => {

expect(paymentIntent.setup_future_usage).toBeUndefined()
})

test('confirmPayment sets setup_future_usage to off_session when futureUsageOffSession is true', async () => {
const ref = React.createRef()
setupConfirmPaymentMocks()

// eslint-disable-next-line @typescript-eslint/no-var-requires
const useShopperConfigurationModule = require('@salesforce/retail-react-app/app/hooks/use-shopper-configuration')
const originalMock = useShopperConfigurationModule.useShopperConfiguration

useShopperConfigurationModule.useShopperConfiguration = jest.fn((configId) => {
if (configId === 'futureUsageOffSession') return true
if (configId === 'cardCaptureAutomatic') return true
if (configId === 'zoneId') return 'default'
return undefined
})

renderWithCheckoutContext(
<SFPaymentsSheet
ref={ref}
onCreateOrder={mockOnCreateOrder}
onError={mockOnError}
/>
)

await waitFor(() => {
expect(ref.current).toBeDefined()
})

await waitFor(() => {
expect(mockCheckout).toHaveBeenCalled()
})

const paymentElement = mockCheckout.mock.calls[0][4]

await act(async () => {
paymentElement.dispatchEvent(
new CustomEvent('sfp:paymentmethodselected', {
bubbles: true,
composed: true,
detail: {
selectedPaymentMethod: 'card',
savePaymentMethodForFutureUse: true
}
})
)
})

await ref.current.confirmPayment()

await waitFor(() => {
expect(mockCheckoutConfirm).toHaveBeenCalled()
})

const confirmCall = mockCheckoutConfirm.mock.calls[0]
const paymentIntentFunction = confirmCall[0]
const paymentIntent = await paymentIntentFunction()

expect(paymentIntent.setup_future_usage).toBe('off_session')

useShopperConfigurationModule.useShopperConfiguration = originalMock
})
})

describe('SPM (Saved Payment Methods) Display', () => {
beforeEach(() => {
jest.clearAllMocks()
mockCustomer.paymentMethodReferences = []
mockUseCustomer.mockImplementation(() => ({
data: {...mockCustomer}
}))
})

test('passes empty savedPaymentMethods to SDK when customer has no payment method references', async () => {
mockCustomer.paymentMethodReferences = []

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

await waitFor(() => {
expect(mockCheckout).toHaveBeenCalled()
})

const checkoutCall = mockCheckout.mock.calls[0]
const config = checkoutCall[2]

expect(config.options.savedPaymentMethods).toEqual([])
})

test('passes empty savedPaymentMethods to SDK when paymentMethodReferences is null', async () => {
mockCustomer.paymentMethodReferences = null

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

await waitFor(() => {
expect(mockCheckout).toHaveBeenCalled()
})

const checkoutCall = mockCheckout.mock.calls[0]
const config = checkoutCall[2]

expect(config.options.savedPaymentMethods).toEqual([])
})

test('passes empty savedPaymentMethods to SDK when paymentMethodReferences is undefined', async () => {
mockCustomer.paymentMethodReferences = undefined

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

await waitFor(() => {
expect(mockCheckout).toHaveBeenCalled()
})

const checkoutCall = mockCheckout.mock.calls[0]
const config = checkoutCall[2]

expect(config.options.savedPaymentMethods).toEqual([])
})

test('passes empty savedPaymentMethods to SDK when paymentMethodSetAccounts is missing', async () => {
mockCustomer.paymentMethodReferences = [
{
id: 'pm_123',
accountId: 'stripe-account-1',
type: 'card',
brand: 'visa',
last4: '4242'
}
]

jest.spyOn(
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('@salesforce/commerce-sdk-react'),
'usePaymentConfiguration'
).mockReturnValue({
data: {
paymentMethods: [
{
id: 'card',
name: 'Card',
paymentMethodType: 'card',
accountId: 'stripe-account-1'
}
],
paymentMethodSetAccounts: null
}
})

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

await waitFor(() => {
expect(mockCheckout).toHaveBeenCalled()
})

const checkoutCall = mockCheckout.mock.calls[0]
const config = checkoutCall[2]

expect(config.options.savedPaymentMethods).toEqual([])
})
})

describe('lifecycle', () => {
Expand Down
Loading
Loading