From 697fae58c9677dff37f6d21c82dfb0f1f3e74f74 Mon Sep 17 00:00:00 2001 From: guilhermer Date: Wed, 6 Nov 2024 14:14:22 +0100 Subject: [PATCH 1/8] draft stories --- packages/lib/src/components/PayPal/types.ts | 6 - .../components/PayPalFastlane/FastlaneSDK.ts | 81 ++++++++++ .../PayPalFastlane/initializeFastlane.ts | 9 ++ .../services/request-fastlane-token.ts | 24 +++ .../src/components/PayPalFastlane/types.ts | 103 +++++++++++++ packages/lib/src/components/index.ts | 4 + packages/lib/src/types/custom.d.ts | 11 ++ .../helpers/create-advanced-checkout.ts | 13 +- .../lib/storybook/helpers/create-checkout.ts | 15 +- packages/lib/storybook/stories/types.ts | 1 + .../wallets/Fastlane/Fastlane.stories.tsx | 17 +++ .../Fastlane/FastlaneInSinglePageApp.tsx | 37 +++++ .../Fastlane/components/CollectEmail.tsx | 76 ++++++++++ .../Fastlane/components/FastlaneStory.scss | 52 +++++++ .../Fastlane/components/GuestShopperForm.tsx | 68 +++++++++ .../wallets/Fastlane/components/Shipping.tsx | 143 ++++++++++++++++++ .../components/ShippingWithFastlane.tsx | 57 +++++++ .../utils/get-fastlane-address-summary.ts | 23 +++ 18 files changed, 720 insertions(+), 20 deletions(-) create mode 100644 packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts create mode 100644 packages/lib/src/components/PayPalFastlane/initializeFastlane.ts create mode 100644 packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts create mode 100644 packages/lib/src/components/PayPalFastlane/types.ts create mode 100644 packages/lib/storybook/stories/wallets/Fastlane/Fastlane.stories.tsx create mode 100644 packages/lib/storybook/stories/wallets/Fastlane/FastlaneInSinglePageApp.tsx create mode 100644 packages/lib/storybook/stories/wallets/Fastlane/components/CollectEmail.tsx create mode 100644 packages/lib/storybook/stories/wallets/Fastlane/components/FastlaneStory.scss create mode 100644 packages/lib/storybook/stories/wallets/Fastlane/components/GuestShopperForm.tsx create mode 100644 packages/lib/storybook/stories/wallets/Fastlane/components/Shipping.tsx create mode 100644 packages/lib/storybook/stories/wallets/Fastlane/components/ShippingWithFastlane.tsx create mode 100644 packages/lib/storybook/stories/wallets/Fastlane/components/utils/get-fastlane-address-summary.ts diff --git a/packages/lib/src/components/PayPal/types.ts b/packages/lib/src/components/PayPal/types.ts index 10aa8c7b97..75bef42420 100644 --- a/packages/lib/src/components/PayPal/types.ts +++ b/packages/lib/src/components/PayPal/types.ts @@ -2,12 +2,6 @@ import { AddressData } from '../../types/global-types'; import { UIElementProps } from '../internal/UIElement/types'; import PaypalElement from './Paypal'; -declare global { - interface Window { - paypal: object; - } -} - export interface PayPalConfiguration extends UIElementProps { /** * Configuration returned by the backend diff --git a/packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts b/packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts new file mode 100644 index 0000000000..6cbc3594c9 --- /dev/null +++ b/packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts @@ -0,0 +1,81 @@ +import { resolveEnvironments } from '../../core/Environment'; +import requestFastlaneToken from './services/request-fastlane-token'; +import Script from '../../utils/Script'; + +import type { Fastlane, AuthenticatedCustomerResult, ShowShippingAddressSelectorResult } from './types'; +import type { FastlaneTokenData } from './services/request-fastlane-token'; +import type { CoreConfiguration } from '../../core/types'; + +export interface FastlaneSDKConfiguration { + clientKey: string; + locale?: 'en_us' | 'es_us' | 'fr_rs' | 'zh_us'; + environment?: CoreConfiguration['environment']; +} + +class FastlaneSDK { + private readonly clientKey: string; + private readonly checkoutShopperURL: string; + private readonly locale: string; + + private fastlaneSdk: Fastlane; + + constructor(configuration: FastlaneSDKConfiguration) { + const { apiUrl } = resolveEnvironments(configuration.environment); + + this.checkoutShopperURL = apiUrl; + this.clientKey = configuration.clientKey; + this.locale = configuration.locale || 'en_us'; + } + + public async initialize(): Promise { + const tokenData = await this.requestClientToken(); + await this.fetchSdk(tokenData.value, tokenData.clientId); + await this.initializeFastlane(); + return this; + } + + public async authenticate(email: string): Promise { + const { customerContextId } = await this.fastlaneSdk.identity.lookupCustomerByEmail(email); + + if (customerContextId) { + return this.fastlaneSdk.identity.triggerAuthenticationFlow(customerContextId); + } else { + return { + authenticationState: 'not_found', + profileData: undefined + }; + } + } + + public showShippingAddressSelector(): Promise { + if (!this.fastlaneSdk.profile) return null; + return this.fastlaneSdk.profile.showShippingAddressSelector(); + } + + public async mountWatermark(container: HTMLElement | string, options?) { + const component = await this.fastlaneSdk.FastlaneWatermarkComponent(options); + component.render(container); + } + + private requestClientToken(): Promise { + return requestFastlaneToken(this.checkoutShopperURL, this.clientKey); + } + + private async fetchSdk(clientToken: string, clientId: string) { + const url = `https://www.paypal.com/sdk/js?client-id=${clientId}&components=buttons,fastlane`; + const script = new Script(url, 'body', {}, { sdkClientToken: clientToken }); + + try { + await script.load(); + } catch (error) { + console.error(error); + } + } + + private async initializeFastlane() { + this.fastlaneSdk = await window.paypal.Fastlane({}); + this.fastlaneSdk.setLocale(this.locale); + } +} + +export default FastlaneSDK; diff --git a/packages/lib/src/components/PayPalFastlane/initializeFastlane.ts b/packages/lib/src/components/PayPalFastlane/initializeFastlane.ts new file mode 100644 index 0000000000..abe77612a5 --- /dev/null +++ b/packages/lib/src/components/PayPalFastlane/initializeFastlane.ts @@ -0,0 +1,9 @@ +import FastlaneSDK from './FastlaneSDK'; +import type { FastlaneSDKConfiguration } from './FastlaneSDK'; + +async function initializeFastlane(configuration: FastlaneSDKConfiguration): Promise { + const fastlane = new FastlaneSDK(configuration); + return await fastlane.initialize(); +} + +export default initializeFastlane; diff --git a/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts b/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts new file mode 100644 index 0000000000..e7f371d7ab --- /dev/null +++ b/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts @@ -0,0 +1,24 @@ +import { httpPost } from '../../../core/Services/http'; + +export interface FastlaneTokenData { + id: string; + clientId: string; + value: string; + expiresAt: string; +} + +function requestFastlaneToken(url: string, clientKey: string): Promise { + // @ts-ignore ignore for now + const path = `utility/v1/payPalFastlane/tokens?clientKey=${clientKey}`; + + return Promise.resolve({ + id: '2747bd08-783a-45c6-902b-3efbda5497b7', + clientId: 'AXy9hIzWB6h_LjZUHjHmsbsiicSIbL4GKOrcgomEedVjduUinIU4C2llxkW5p0OG0zTNgviYFceaXEnj', + merchantId: 'C3UCKQHMW4948', + value: 'eyJraWQiOiJkMTA2ZTUwNjkzOWYxMWVlYjlkMTAyNDJhYzEyMDAwMiIsInR5cCI6IkpXVCIsImFsZyI6IkVTMjU2In0.eyJpc3MiOiJodHRwczovL2FwaS5zYW5kYm94LnBheXBhbC5jb20iLCJhdWQiOlsiaHR0cHM6Ly9hcGkuYnJhaW50cmVlZ2F0ZXdheS5jb20iLCJjaGVja291dC1wbGF5Z3JvdW5kLm5ldGxpZnkuYXBwIl0sInN1YiI6Ik02VE5BRVNaNUZHTk4iLCJhY3IiOlsiY2xpZW50Il0sInNjb3BlIjpbIkJyYWludHJlZTpWYXVsdCJdLCJvcHRpb25zIjp7fSwiYXoiOiJjY2cxOC5zbGMiLCJleHRlcm5hbF9pZCI6WyJQYXlQYWw6QzNVQ0tRSE1XNDk0OCIsIkJyYWludHJlZTozZGI4aG5rdHJ0bXpzMmd0Il0sImV4cCI6MTczMDg5Nzk4NSwiaWF0IjoxNzMwODk3MDg1LCJqdGkiOiJVMkFBS05JdjBkbjZxaWtEQUMweVctdmJKSWhra3VPYTVSQ2MwMlJNdXVMWWVFUUQ2NE85UjJ1eWtRcFpucjZPanhyT3I3OVdLd0ZadGtwdi1LdUZiWHBHWkxFLU9uUEJEXzdUb1Z0RzI2dE9rM2ZNeHEyaVNna2RUd3UzRk5wQSJ9.p2lVHnIM29OsQQq4Q6N5UeHAs3AWDWs9OZ0DmYG-aMng_Dul6j1zdK4T5WoyWMu8eoM3DXHuRSQfZvD4eUPjLA', + expiresAt: '2024-11-01T13:34:01.804+00:00' + }); + // return httpPost({ loadingContext: url, path, errorLevel: 'fatal' }); +} + +export default requestFastlaneToken; diff --git a/packages/lib/src/components/PayPalFastlane/types.ts b/packages/lib/src/components/PayPalFastlane/types.ts new file mode 100644 index 0000000000..335a4639b4 --- /dev/null +++ b/packages/lib/src/components/PayPalFastlane/types.ts @@ -0,0 +1,103 @@ +export type FastlaneConstructor = (options: FastlaneOptions) => Promise; + +/** + * PayPal Fastlane Reference: + * https://developer.paypal.com/docs/checkout/fastlane/reference/#link-customizeyourintegration + */ + +// TODO: Verify if we pass options here +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface FastlaneOptions {} + +export interface Fastlane { + identity: { + lookupCustomerByEmail: (email: string) => Promise<{ customerContextId: string }>; + triggerAuthenticationFlow: (customerContextId: string, options?: AuthenticationFlowOptions) => Promise; + }; + profile: { + showShippingAddressSelector: () => Promise; + showCardSelector: () => ShowCardSelectorResult; + }; + setLocale: (locale: string) => void; + FastlaneWatermarkComponent: (options: FastlaneWatermarkOptions) => Promise; +} + +interface FastlaneWatermarkOptions { + includeAdditionalInfo: boolean; +} +interface FastlaneWatermarkComponent { + render: (container) => null; +} + +// TODO: fill this in after workshop +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface AuthenticationFlowOptions {} + +/** + * The AuthenticatedCustomerResult object type is returned from the identity.triggerAuthenticationFlow() call. + */ +export interface AuthenticatedCustomerResult { + authenticationState: 'succeeded' | 'failed' | 'canceled' | 'not_found'; + profileData: FastlaneProfile; +} +interface FastlaneProfile { + name: Name; + shippingAddress: FastlaneShipping; + card: PaymentToken; +} + +interface Name { + firstName: string; + lastName: string; + fullName: string; +} + +interface Phone { + nationalNumber: string; + countryCode: string; +} + +export interface FastlaneAddress { + addressLine1: string; + addressLine2: string; + adminArea1: string; + adminArea2: string; + postalCode: string; + countryCode: string; + phone: Phone; +} + +export interface FastlaneShipping { + name: Name; + address: FastlaneAddress; + phoneNumber: Phone; +} + +interface PaymentToken { + id: string; + paymentSource: PaymentSource; +} +interface PaymentSource { + card: CardPaymentSource; +} + +interface CardPaymentSource { + brand: string; + expiry: string; + lastDigits: string; + name: string; + billingAddress: FastlaneAddress; +} + +/** + * Profile method reference types + */ +export interface ShowShippingAddressSelectorResult { + selectionChanged: boolean; + selectedAddress: FastlaneShipping; +} + +interface ShowCardSelectorResult { + selectionChanged: boolean; + selectedCard: PaymentToken; +} diff --git a/packages/lib/src/components/index.ts b/packages/lib/src/components/index.ts index d05155adff..f9254c90c9 100644 --- a/packages/lib/src/components/index.ts +++ b/packages/lib/src/components/index.ts @@ -90,6 +90,10 @@ export { default as ANCV } from './ANCV'; export { default as Giftcard } from './Giftcard'; export { default as MealVoucherFR } from './MealVoucherFR'; +/** Utilities */ +export { default as initializeFastlane } from './PayPalFastlane/initializeFastlane'; +export { default as FastlaneSDK } from './PayPalFastlane/FastlaneSDK'; + /** Internal */ export { default as Address } from './Address'; export { default as BankTransfer } from './BankTransfer'; diff --git a/packages/lib/src/types/custom.d.ts b/packages/lib/src/types/custom.d.ts index 0e04382135..84a7f964db 100644 --- a/packages/lib/src/types/custom.d.ts +++ b/packages/lib/src/types/custom.d.ts @@ -1,8 +1,19 @@ +import type { FastlaneConstructor } from '../components/PayPalFastlane/types'; + declare module '*.scss' { const content: { [className: string]: string }; export default content; } +declare global { + interface Window { + ApplePaySession?: ApplePaySession; + paypal?: { + Fastlane?: FastlaneConstructor; + }; + } +} + interface Window { AdyenWeb: any; VISA_SDK?: { diff --git a/packages/lib/storybook/helpers/create-advanced-checkout.ts b/packages/lib/storybook/helpers/create-advanced-checkout.ts index c3cef699be..14df554433 100644 --- a/packages/lib/storybook/helpers/create-advanced-checkout.ts +++ b/packages/lib/storybook/helpers/create-advanced-checkout.ts @@ -11,6 +11,7 @@ async function createAdvancedFlowCheckout({ countryCode, shopperLocale, amount, + allowedPaymentTypes = [], paymentMethodsOverride, ...restCheckoutProps }: AdyenCheckoutProps): Promise { @@ -28,16 +29,12 @@ async function createAdvancedFlowCheckout({ const paymentMethodsResponse = !paymentMethodsOverride ? _paymentMethodsResponse : { - storedPaymentMethods: [ - ...(_paymentMethodsResponse.storedPaymentMethods ? _paymentMethodsResponse.storedPaymentMethods : []), - ...(paymentMethodsOverride.storedPaymentMethods ? paymentMethodsOverride.storedPaymentMethods : []) - ], - paymentMethods: [ - ...(_paymentMethodsResponse.paymentMethods ? _paymentMethodsResponse.paymentMethods : []), - ...(paymentMethodsOverride.paymentMethods ? paymentMethodsOverride.paymentMethods : []) - ] + storedPaymentMethods: paymentMethodsOverride.storedPaymentMethods ? paymentMethodsOverride.storedPaymentMethods : [], + paymentMethods: paymentMethodsOverride.paymentMethods ? paymentMethodsOverride.paymentMethods : [] }; + paymentMethodsResponse.paymentMethods = paymentMethodsResponse.paymentMethods.filter(pm => allowedPaymentTypes.includes(pm.type)); + const checkout = await AdyenCheckout({ clientKey: process.env.CLIENT_KEY, // @ts-ignore CLIENT_ENV has valid value diff --git a/packages/lib/storybook/helpers/create-checkout.ts b/packages/lib/storybook/helpers/create-checkout.ts index 5922bfcb69..c8d5b4894a 100644 --- a/packages/lib/storybook/helpers/create-checkout.ts +++ b/packages/lib/storybook/helpers/create-checkout.ts @@ -6,15 +6,18 @@ import Core from '../../src/core'; async function createCheckout(checkoutConfig: GlobalStoryProps): Promise { const { useSessions, ...rest } = checkoutConfig; - const overidenPaymentMethodsAmount = + const overriddenPaymentMethodsAmount = (rest.paymentMethodsOverride?.paymentMethods?.length || 0) + (rest.paymentMethodsOverride?.storedPaymentMethods?.length || 0); - const hasPaymentOveride = overidenPaymentMethodsAmount > 0; + const hasPaymentOverridden = overriddenPaymentMethodsAmount > 0; - if (useSessions && !hasPaymentOveride) { - return await createSessionsCheckout(rest); - } else if (useSessions && hasPaymentOveride) { - console.warn('🟢 Checkout Storybook: paymentMethodsOverride is defined while using Sessions, forcing advance flow.'); + if (useSessions) { + if (!hasPaymentOverridden && !rest.allowedPaymentTypes) { + return await createSessionsCheckout(rest); + } else { + console.warn('🟢 Checkout Storybook: Forcing advance flow.'); + } } + return await createAdvancedFlowCheckout(rest); } diff --git a/packages/lib/storybook/stories/types.ts b/packages/lib/storybook/stories/types.ts index 91accade3e..9251fb0fc2 100644 --- a/packages/lib/storybook/stories/types.ts +++ b/packages/lib/storybook/stories/types.ts @@ -27,6 +27,7 @@ export type AdyenCheckoutProps = { shopperLocale: string; amount: number; sessionData?: PaymentMethodsResponse; + allowedPaymentTypes?: string[]; paymentMethodsOverride?: PaymentMethodsResponse; onPaymentCompleted?: (data: any, element?: UIElement) => void; }; diff --git a/packages/lib/storybook/stories/wallets/Fastlane/Fastlane.stories.tsx b/packages/lib/storybook/stories/wallets/Fastlane/Fastlane.stories.tsx new file mode 100644 index 0000000000..71a6462762 --- /dev/null +++ b/packages/lib/storybook/stories/wallets/Fastlane/Fastlane.stories.tsx @@ -0,0 +1,17 @@ +import { MetaConfiguration, StoryConfiguration } from '../../types'; +import { FastlaneInSinglePageApp } from './FastlaneInSinglePageApp'; + +type FastlaneStory = StoryConfiguration<{}>; + +const meta: MetaConfiguration = { + title: 'Wallets/Fastlane' +}; + +export const Default: FastlaneStory = { + render: checkoutConfig => { + const allowedPaymentTypes = ['scheme', 'paypal']; + return ; + } +}; + +export default meta; diff --git a/packages/lib/storybook/stories/wallets/Fastlane/FastlaneInSinglePageApp.tsx b/packages/lib/storybook/stories/wallets/Fastlane/FastlaneInSinglePageApp.tsx new file mode 100644 index 0000000000..fc53f2317f --- /dev/null +++ b/packages/lib/storybook/stories/wallets/Fastlane/FastlaneInSinglePageApp.tsx @@ -0,0 +1,37 @@ +import { h } from 'preact'; +import { useState } from 'preact/hooks'; + +import Dropin from '../../../../src/components/Dropin'; +import Card from '../../../../src/components/Card'; +import PayPal from '../../../../src/components/PayPal'; + +import { Checkout } from '../../Checkout'; +import { ComponentContainer } from '../../ComponentContainer'; +import { GuestShopperForm } from './components/GuestShopperForm'; +import './components/FastlaneStory.scss'; + +import { GlobalStoryProps } from '../../types'; + +interface Props { + checkoutConfig: GlobalStoryProps; +} + +export const FastlaneInSinglePageApp = ({ checkoutConfig }: Props) => { + const [fastlaneData, setFastlaneData] = useState(null); + + const handleOnCheckoutStep = (fastlaneSdk, fastlaneData) => { + setFastlaneData(fastlaneData); + }; + + if (!fastlaneData) { + return ; + } + + return ( + + {checkout => ( + + )} + + ); +}; diff --git a/packages/lib/storybook/stories/wallets/Fastlane/components/CollectEmail.tsx b/packages/lib/storybook/stories/wallets/Fastlane/components/CollectEmail.tsx new file mode 100644 index 0000000000..bbb2094cee --- /dev/null +++ b/packages/lib/storybook/stories/wallets/Fastlane/components/CollectEmail.tsx @@ -0,0 +1,76 @@ +import { h } from 'preact'; +import { useEffect, useState } from 'preact/hooks'; + +import FastlaneSDK from '../../../../../src/components/PayPalFastlane/FastlaneSDK'; +import type { AuthenticatedCustomerResult } from '../../../../../src/components/PayPalFastlane/types'; + +interface CollectEmailProps { + fastlaneSdk: FastlaneSDK; + onFastlaneLookup: (authResult: AuthenticatedCustomerResult) => void; + onEditEmail: () => void; +} + +export const CollectEmail = ({ fastlaneSdk, onFastlaneLookup, onEditEmail }: CollectEmailProps) => { + const [email, setEmail] = useState(null); + const [viewOnly, setViewOnly] = useState(false); + + const renderWatermark = async () => { + await fastlaneSdk.mountWatermark('#watermark-container', { + includeAdditionalInfo: true + }); + }; + + const handleEmailInput = event => { + setEmail(event.currentTarget.value); + }; + + const handleEditEmail = () => { + setViewOnly(false); + onEditEmail(); + }; + + const handleButtonClick = async () => { + const authResult = await fastlaneSdk.authenticate(email); + onFastlaneLookup(authResult); + setViewOnly(true); + }; + + useEffect(() => { + void renderWatermark(); + }, []); + + return ( + <> +
+

Customer

+ {viewOnly && ( + + )} +
+
+
+ +
+
+ + {!viewOnly && ( + + )} +
+ {!viewOnly && } + + ); +}; diff --git a/packages/lib/storybook/stories/wallets/Fastlane/components/FastlaneStory.scss b/packages/lib/storybook/stories/wallets/Fastlane/components/FastlaneStory.scss new file mode 100644 index 0000000000..57f6bb1891 --- /dev/null +++ b/packages/lib/storybook/stories/wallets/Fastlane/components/FastlaneStory.scss @@ -0,0 +1,52 @@ +.email-container { + display: flex; +} + +.email-input-wrapper { + display: flex; + flex-direction: column; +} + +#watermark-container { + align-self: end; +} + +.email-input { + background-color: white; + border: #aab2bc 1px solid; + border-radius: 4px; + outline: none; + padding: 10px; + position: relative; + text-overflow: ellipsis; + width: 300px; + margin-right: 12px; +} + +.form-container { + background-color: white; + padding: 20px; + border-radius: 8px; + border: #0d419d 1px solid; + max-width: 800px; +} + +.button { + cursor: pointer; + width: 145px; + height: 37px; + border-radius: 4px; + border: 1px solid #2d7fec; + background-color: #0551b5; + color: white; +} + +.button:hover { + background-color: #2d7fec; +} + +.section_header { + display: flex; + justify-content: space-between; + align-items: center; +} diff --git a/packages/lib/storybook/stories/wallets/Fastlane/components/GuestShopperForm.tsx b/packages/lib/storybook/stories/wallets/Fastlane/components/GuestShopperForm.tsx new file mode 100644 index 0000000000..1a40f0cff8 --- /dev/null +++ b/packages/lib/storybook/stories/wallets/Fastlane/components/GuestShopperForm.tsx @@ -0,0 +1,68 @@ +import { h } from 'preact'; +import { useEffect, useState } from 'preact/hooks'; +import './FastlaneStory.scss'; + +import initializeFastlane from '../../../../../src/components/PayPalFastlane/initializeFastlane'; +import FastlaneSDK from '../../../../../src/components/PayPalFastlane/FastlaneSDK'; +import type { AuthenticatedCustomerResult } from '../../../../../src/components/PayPalFastlane/types'; + +import { CollectEmail } from './CollectEmail'; +import { Shipping } from './Shipping'; +import { ShippingWithFastlane } from './ShippingWithFastlane'; + +interface GuestShopperFormProps { + onCheckoutStep(fastlane: FastlaneSDK, data: any): void; +} + +export const GuestShopperForm = ({ onCheckoutStep }: GuestShopperFormProps) => { + const [fastlane, setFastlane] = useState(null); + const [fastlaneLookupData, setFastlaneLookupData] = useState(null); + + const loadFastlane = async () => { + const sdk = await initializeFastlane({ + clientKey: 'test_JC3ZFTA6WFCCRN454MVDEYOWEI5D3LT2', // Joost clientkey + environment: 'test' + }); + setFastlane(sdk); + }; + + const handleOnEditEmail = () => { + setFastlaneLookupData(null); + }; + + const handleFastlaneLookup = data => { + console.log(data); + setFastlaneLookupData(data); + }; + + const handleOnCheckoutClick = shippingAddress => { + onCheckoutStep(fastlane, fastlaneLookupData); + }; + + useEffect(() => { + void loadFastlane().catch(error => { + console.log(error); + }); + }, []); + + if (!fastlane) { + return null; + } + + return ( +
+ +
+ + {fastlaneLookupData?.authenticationState === 'succeeded' && ( + + )} + + {fastlaneLookupData?.authenticationState === 'not_found' && } +
+ ); +}; diff --git a/packages/lib/storybook/stories/wallets/Fastlane/components/Shipping.tsx b/packages/lib/storybook/stories/wallets/Fastlane/components/Shipping.tsx new file mode 100644 index 0000000000..2e571dc66d --- /dev/null +++ b/packages/lib/storybook/stories/wallets/Fastlane/components/Shipping.tsx @@ -0,0 +1,143 @@ +import { h } from 'preact'; +import { useState } from 'preact/hooks'; + +interface ShippingProps { + onCheckoutClick: (shippingAddress?: any) => void; +} + +export const Shipping = ({ onCheckoutClick }: ShippingProps) => { + const [isShippingRequired, setIsShippingRequired] = useState(true); + const [formData, setFormData] = useState({ + givenName: '', + familyName: '', + addressLine1: '', + addressLine2: '', + addressLevel1: '', + addressLevel2: '', + postalCode: '', + country: '', + telCountryCode: '', + telNational: '' + }); + + const handleShippingCheckboxClick = () => { + setIsShippingRequired(!isShippingRequired); + }; + + const handleChange = e => { + const { name, value } = e.target; + setFormData(prevState => ({ ...prevState, [name]: value })); + }; + + const fillInMockData = () => { + setFormData({ + givenName: 'John', + familyName: 'Doe', + addressLine1: 'Simon Carmilgestraat 10', + addressLine2: 'Ap 29', + addressLevel1: 'Noord Holand', + addressLevel2: 'Amsterdam', + postalCode: '1028 PX', + country: 'Netherlands', + telCountryCode: '31', + telNational: '611223399' + }); + }; + + const handleOnCheckoutClick = () => { + onCheckoutClick(formData); + }; + + return ( +
+

Shipping Details

+ +
+ + +
+ + {isShippingRequired && ( + <> +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + )} + + + + +
+ ); +}; diff --git a/packages/lib/storybook/stories/wallets/Fastlane/components/ShippingWithFastlane.tsx b/packages/lib/storybook/stories/wallets/Fastlane/components/ShippingWithFastlane.tsx new file mode 100644 index 0000000000..f2b6067108 --- /dev/null +++ b/packages/lib/storybook/stories/wallets/Fastlane/components/ShippingWithFastlane.tsx @@ -0,0 +1,57 @@ +import { h } from 'preact'; +import { useEffect, useState } from 'preact/hooks'; +import getAddressSummary from './utils/get-fastlane-address-summary'; + +import FastlaneSDK from '../../../../../src/components/PayPalFastlane/FastlaneSDK'; + +interface ShippingWithFastlaneProps { + fastlaneSdk: FastlaneSDK; + address: any; + onCheckoutClick: () => void; +} + +export const ShippingWithFastlane = ({ fastlaneSdk, address, onCheckoutClick }: ShippingWithFastlaneProps) => { + const [addressSummary, setAddressSummary] = useState(null); + + useEffect(() => { + const summary = getAddressSummary(address); + setAddressSummary(summary); + }, [address]); + + const handleShippingClick = async () => { + const data = await fastlaneSdk.showShippingAddressSelector(); + console.log(data); + + if (data.selectionChanged) { + const summary = getAddressSummary(data.selectedAddress); + setAddressSummary(summary); + } + }; + + const handleCheckoutClick = () => { + onCheckoutClick(); + }; + + return ( +
+
+

Shipping Details

+ + {addressSummary && ( + + )} +
+ + {addressSummary && ( +
+
{addressSummary}
+ +
+ )} +
+ ); +}; diff --git a/packages/lib/storybook/stories/wallets/Fastlane/components/utils/get-fastlane-address-summary.ts b/packages/lib/storybook/stories/wallets/Fastlane/components/utils/get-fastlane-address-summary.ts new file mode 100644 index 0000000000..78d6390c90 --- /dev/null +++ b/packages/lib/storybook/stories/wallets/Fastlane/components/utils/get-fastlane-address-summary.ts @@ -0,0 +1,23 @@ +import { FastlaneShipping } from '../../../../../../src/components/PayPalFastlane/types'; + +/** + * Format the PayPal address to show nicely in the Storybook UI + */ +const getAddressSummary = (shipping: FastlaneShipping) => { + if (!shipping) return null; + + const { firstName, lastName, fullName } = shipping.name; + const { addressLine1, addressLine2, adminArea2, adminArea1, postalCode, countryCode } = shipping.address; + const { countryCode: telCountryCode, nationalNumber } = shipping.phoneNumber; + + const isNotEmpty = field => !!field; + const summary = [ + fullName || [firstName, lastName].filter(isNotEmpty).join(' '), + [addressLine1, addressLine2].filter(isNotEmpty).join(', '), + [adminArea2, [adminArea1, postalCode].filter(isNotEmpty).join(' '), countryCode].filter(isNotEmpty).join(', '), + [telCountryCode, nationalNumber].filter(isNotEmpty).join('') + ]; + return summary.filter(isNotEmpty).join('\n'); +}; + +export default getAddressSummary; From 0b5a750e4574dc45a1445b9050dd116fc0fae3ac Mon Sep 17 00:00:00 2001 From: guilhermer Date: Thu, 7 Nov 2024 15:15:17 +0100 Subject: [PATCH 2/8] cleaned up stories and code --- .../components/PayPalFastlane/FastlaneSDK.ts | 17 ++-- .../services/request-fastlane-token.ts | 2 +- .../src/components/PayPalFastlane/types.ts | 29 +++---- .../src/components/PayPalFastlane/utils.ts | 3 + .../Fastlane/FastlaneInSinglePageApp.tsx | 7 +- .../Fastlane/components/CollectEmail.tsx | 14 ++-- .../Fastlane/components/FastlaneStory.scss | 14 +++- .../Fastlane/components/GuestShopperForm.tsx | 20 ++--- .../wallets/Fastlane/components/Shipping.tsx | 79 ++++++++++++++----- .../components/ShippingWithFastlane.tsx | 23 +++--- 10 files changed, 127 insertions(+), 81 deletions(-) create mode 100644 packages/lib/src/components/PayPalFastlane/utils.ts diff --git a/packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts b/packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts index 6cbc3594c9..686a7b8191 100644 --- a/packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts +++ b/packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts @@ -1,14 +1,15 @@ import { resolveEnvironments } from '../../core/Environment'; import requestFastlaneToken from './services/request-fastlane-token'; +import { convertAdyenLocaleToFastlaneLocale } from './utils'; import Script from '../../utils/Script'; -import type { Fastlane, AuthenticatedCustomerResult, ShowShippingAddressSelectorResult } from './types'; +import type { Fastlane, FastlaneAuthenticatedCustomerResult, FastlaneShippingAddressSelectorResult } from './types'; import type { FastlaneTokenData } from './services/request-fastlane-token'; import type { CoreConfiguration } from '../../core/types'; export interface FastlaneSDKConfiguration { clientKey: string; - locale?: 'en_us' | 'es_us' | 'fr_rs' | 'zh_us'; + locale?: 'en-US' | 'es-US' | 'fr-RS' | 'zh-US'; environment?: CoreConfiguration['environment']; } @@ -24,7 +25,7 @@ class FastlaneSDK { this.checkoutShopperURL = apiUrl; this.clientKey = configuration.clientKey; - this.locale = configuration.locale || 'en_us'; + this.locale = convertAdyenLocaleToFastlaneLocale(configuration.locale || 'en-US'); } public async initialize(): Promise { @@ -34,7 +35,7 @@ class FastlaneSDK { return this; } - public async authenticate(email: string): Promise { + public async authenticate(email: string): Promise { const { customerContextId } = await this.fastlaneSdk.identity.lookupCustomerByEmail(email); if (customerContextId) { @@ -47,12 +48,12 @@ class FastlaneSDK { } } - public showShippingAddressSelector(): Promise { + public showShippingAddressSelector(): Promise { if (!this.fastlaneSdk.profile) return null; return this.fastlaneSdk.profile.showShippingAddressSelector(); } - public async mountWatermark(container: HTMLElement | string, options?) { + public async mountWatermark(container: HTMLElement | string, options = { includeAdditionalInfo: false }): Promise { const component = await this.fastlaneSdk.FastlaneWatermarkComponent(options); component.render(container); } @@ -61,7 +62,7 @@ class FastlaneSDK { return requestFastlaneToken(this.checkoutShopperURL, this.clientKey); } - private async fetchSdk(clientToken: string, clientId: string) { + private async fetchSdk(clientToken: string, clientId: string): Promise { const url = `https://www.paypal.com/sdk/js?client-id=${clientId}&components=buttons,fastlane`; const script = new Script(url, 'body', {}, { sdkClientToken: clientToken }); @@ -72,7 +73,7 @@ class FastlaneSDK { } } - private async initializeFastlane() { + private async initializeFastlane(): Promise { this.fastlaneSdk = await window.paypal.Fastlane({}); this.fastlaneSdk.setLocale(this.locale); } diff --git a/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts b/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts index e7f371d7ab..bf4d68d963 100644 --- a/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts +++ b/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts @@ -15,7 +15,7 @@ function requestFastlaneToken(url: string, clientKey: string): Promise({ loadingContext: url, path, errorLevel: 'fatal' }); diff --git a/packages/lib/src/components/PayPalFastlane/types.ts b/packages/lib/src/components/PayPalFastlane/types.ts index 335a4639b4..ca051478f9 100644 --- a/packages/lib/src/components/PayPalFastlane/types.ts +++ b/packages/lib/src/components/PayPalFastlane/types.ts @@ -12,19 +12,16 @@ export interface FastlaneOptions {} export interface Fastlane { identity: { lookupCustomerByEmail: (email: string) => Promise<{ customerContextId: string }>; - triggerAuthenticationFlow: (customerContextId: string, options?: AuthenticationFlowOptions) => Promise; + triggerAuthenticationFlow: (customerContextId: string, options?: AuthenticationFlowOptions) => Promise; }; profile: { - showShippingAddressSelector: () => Promise; + showShippingAddressSelector: () => Promise; showCardSelector: () => ShowCardSelectorResult; }; setLocale: (locale: string) => void; - FastlaneWatermarkComponent: (options: FastlaneWatermarkOptions) => Promise; + FastlaneWatermarkComponent: (options: { includeAdditionalInfo: boolean }) => Promise; } -interface FastlaneWatermarkOptions { - includeAdditionalInfo: boolean; -} interface FastlaneWatermarkComponent { render: (container) => null; } @@ -36,10 +33,6 @@ interface AuthenticationFlowOptions {} /** * The AuthenticatedCustomerResult object type is returned from the identity.triggerAuthenticationFlow() call. */ -export interface AuthenticatedCustomerResult { - authenticationState: 'succeeded' | 'failed' | 'canceled' | 'not_found'; - profileData: FastlaneProfile; -} interface FastlaneProfile { name: Name; shippingAddress: FastlaneShipping; @@ -89,15 +82,19 @@ interface CardPaymentSource { billingAddress: FastlaneAddress; } +interface ShowCardSelectorResult { + selectionChanged: boolean; + selectedCard: PaymentToken; +} + /** - * Profile method reference types + * External types */ -export interface ShowShippingAddressSelectorResult { +export interface FastlaneShippingAddressSelectorResult { selectionChanged: boolean; selectedAddress: FastlaneShipping; } - -interface ShowCardSelectorResult { - selectionChanged: boolean; - selectedCard: PaymentToken; +export interface FastlaneAuthenticatedCustomerResult { + authenticationState: 'succeeded' | 'failed' | 'canceled' | 'not_found'; + profileData: FastlaneProfile; } diff --git a/packages/lib/src/components/PayPalFastlane/utils.ts b/packages/lib/src/components/PayPalFastlane/utils.ts new file mode 100644 index 0000000000..ada3b678e7 --- /dev/null +++ b/packages/lib/src/components/PayPalFastlane/utils.ts @@ -0,0 +1,3 @@ +export function convertAdyenLocaleToFastlaneLocale(locale: string) { + return locale.replace('-', '_').toLowerCase(); +} diff --git a/packages/lib/storybook/stories/wallets/Fastlane/FastlaneInSinglePageApp.tsx b/packages/lib/storybook/stories/wallets/Fastlane/FastlaneInSinglePageApp.tsx index fc53f2317f..ca95f26906 100644 --- a/packages/lib/storybook/stories/wallets/Fastlane/FastlaneInSinglePageApp.tsx +++ b/packages/lib/storybook/stories/wallets/Fastlane/FastlaneInSinglePageApp.tsx @@ -1,5 +1,6 @@ import { h } from 'preact'; import { useState } from 'preact/hooks'; +import { GlobalStoryProps } from '../../types'; import Dropin from '../../../../src/components/Dropin'; import Card from '../../../../src/components/Card'; @@ -8,9 +9,6 @@ import PayPal from '../../../../src/components/PayPal'; import { Checkout } from '../../Checkout'; import { ComponentContainer } from '../../ComponentContainer'; import { GuestShopperForm } from './components/GuestShopperForm'; -import './components/FastlaneStory.scss'; - -import { GlobalStoryProps } from '../../types'; interface Props { checkoutConfig: GlobalStoryProps; @@ -19,7 +17,8 @@ interface Props { export const FastlaneInSinglePageApp = ({ checkoutConfig }: Props) => { const [fastlaneData, setFastlaneData] = useState(null); - const handleOnCheckoutStep = (fastlaneSdk, fastlaneData) => { + const handleOnCheckoutStep = (fastlaneSdk, fastlaneData, shippingAddress) => { + console.log('FastlaneInSinglePageApp', fastlaneSdk, fastlaneData, shippingAddress); setFastlaneData(fastlaneData); }; diff --git a/packages/lib/storybook/stories/wallets/Fastlane/components/CollectEmail.tsx b/packages/lib/storybook/stories/wallets/Fastlane/components/CollectEmail.tsx index bbb2094cee..01ef0d0434 100644 --- a/packages/lib/storybook/stories/wallets/Fastlane/components/CollectEmail.tsx +++ b/packages/lib/storybook/stories/wallets/Fastlane/components/CollectEmail.tsx @@ -2,11 +2,11 @@ import { h } from 'preact'; import { useEffect, useState } from 'preact/hooks'; import FastlaneSDK from '../../../../../src/components/PayPalFastlane/FastlaneSDK'; -import type { AuthenticatedCustomerResult } from '../../../../../src/components/PayPalFastlane/types'; +import type { FastlaneAuthenticatedCustomerResult } from '../../../../../src/components/PayPalFastlane/types'; interface CollectEmailProps { fastlaneSdk: FastlaneSDK; - onFastlaneLookup: (authResult: AuthenticatedCustomerResult) => void; + onFastlaneLookup: (authResult: FastlaneAuthenticatedCustomerResult) => void; onEditEmail: () => void; } @@ -15,9 +15,7 @@ export const CollectEmail = ({ fastlaneSdk, onFastlaneLookup, onEditEmail }: Col const [viewOnly, setViewOnly] = useState(false); const renderWatermark = async () => { - await fastlaneSdk.mountWatermark('#watermark-container', { - includeAdditionalInfo: true - }); + await fastlaneSdk.mountWatermark('#watermark-container'); }; const handleEmailInput = event => { @@ -40,7 +38,7 @@ export const CollectEmail = ({ fastlaneSdk, onFastlaneLookup, onEditEmail }: Col }, []); return ( - <> +

Customer

{viewOnly && ( @@ -53,7 +51,7 @@ export const CollectEmail = ({ fastlaneSdk, onFastlaneLookup, onEditEmail }: Col
{!viewOnly && } - +
); }; diff --git a/packages/lib/storybook/stories/wallets/Fastlane/components/FastlaneStory.scss b/packages/lib/storybook/stories/wallets/Fastlane/components/FastlaneStory.scss index 57f6bb1891..1f5a119d2c 100644 --- a/packages/lib/storybook/stories/wallets/Fastlane/components/FastlaneStory.scss +++ b/packages/lib/storybook/stories/wallets/Fastlane/components/FastlaneStory.scss @@ -11,7 +11,7 @@ align-self: end; } -.email-input { +.input-field { background-color: white; border: #aab2bc 1px solid; border-radius: 4px; @@ -23,6 +23,17 @@ margin-right: 12px; } +.shipping-section { + .input-field { + margin-bottom: 12px; + } + + .shipping-checkbox { + margin-bottom: 20px; + } +} + + .form-container { background-color: white; padding: 20px; @@ -39,6 +50,7 @@ border: 1px solid #2d7fec; background-color: #0551b5; color: white; + margin-bottom: 12px; } .button:hover { diff --git a/packages/lib/storybook/stories/wallets/Fastlane/components/GuestShopperForm.tsx b/packages/lib/storybook/stories/wallets/Fastlane/components/GuestShopperForm.tsx index 1a40f0cff8..6b5245824a 100644 --- a/packages/lib/storybook/stories/wallets/Fastlane/components/GuestShopperForm.tsx +++ b/packages/lib/storybook/stories/wallets/Fastlane/components/GuestShopperForm.tsx @@ -1,22 +1,22 @@ import { h } from 'preact'; import { useEffect, useState } from 'preact/hooks'; -import './FastlaneStory.scss'; - -import initializeFastlane from '../../../../../src/components/PayPalFastlane/initializeFastlane'; -import FastlaneSDK from '../../../../../src/components/PayPalFastlane/FastlaneSDK'; -import type { AuthenticatedCustomerResult } from '../../../../../src/components/PayPalFastlane/types'; import { CollectEmail } from './CollectEmail'; import { Shipping } from './Shipping'; import { ShippingWithFastlane } from './ShippingWithFastlane'; +import './FastlaneStory.scss'; + +import initializeFastlane from '../../../../../src/components/PayPalFastlane/initializeFastlane'; +import FastlaneSDK from '../../../../../src/components/PayPalFastlane/FastlaneSDK'; +import type { FastlaneAuthenticatedCustomerResult } from '../../../../../src/components/PayPalFastlane/types'; interface GuestShopperFormProps { - onCheckoutStep(fastlane: FastlaneSDK, data: any): void; + onCheckoutStep(fastlane: FastlaneSDK, fastlaneData: any, shippingAddress: any): void; } export const GuestShopperForm = ({ onCheckoutStep }: GuestShopperFormProps) => { const [fastlane, setFastlane] = useState(null); - const [fastlaneLookupData, setFastlaneLookupData] = useState(null); + const [fastlaneLookupData, setFastlaneLookupData] = useState(null); const loadFastlane = async () => { const sdk = await initializeFastlane({ @@ -31,12 +31,11 @@ export const GuestShopperForm = ({ onCheckoutStep }: GuestShopperFormProps) => { }; const handleFastlaneLookup = data => { - console.log(data); setFastlaneLookupData(data); }; - const handleOnCheckoutClick = shippingAddress => { - onCheckoutStep(fastlane, fastlaneLookupData); + const handleOnCheckoutClick = (shippingAddress?: any) => { + onCheckoutStep(fastlane, fastlaneLookupData, shippingAddress); }; useEffect(() => { @@ -51,6 +50,7 @@ export const GuestShopperForm = ({ onCheckoutStep }: GuestShopperFormProps) => { return (
+

Merchant Checkout Page


diff --git a/packages/lib/storybook/stories/wallets/Fastlane/components/Shipping.tsx b/packages/lib/storybook/stories/wallets/Fastlane/components/Shipping.tsx index 2e571dc66d..bfbf41c726 100644 --- a/packages/lib/storybook/stories/wallets/Fastlane/components/Shipping.tsx +++ b/packages/lib/storybook/stories/wallets/Fastlane/components/Shipping.tsx @@ -44,16 +44,18 @@ export const Shipping = ({ onCheckoutClick }: ShippingProps) => { }); }; - const handleOnCheckoutClick = () => { + const handleOnSubmit = e => { + e.preventDefault(); onCheckoutClick(formData); }; return ( -
+

Shipping Details

{
- {isShippingRequired && ( - <> + {isShippingRequired ? ( +
- +
- - + +
- - )} - - - -
+ +
+ +
+ + ) : ( + + )} + ); }; diff --git a/packages/lib/storybook/stories/wallets/Fastlane/components/ShippingWithFastlane.tsx b/packages/lib/storybook/stories/wallets/Fastlane/components/ShippingWithFastlane.tsx index f2b6067108..59bb96e578 100644 --- a/packages/lib/storybook/stories/wallets/Fastlane/components/ShippingWithFastlane.tsx +++ b/packages/lib/storybook/stories/wallets/Fastlane/components/ShippingWithFastlane.tsx @@ -1,39 +1,36 @@ import { h } from 'preact'; -import { useEffect, useState } from 'preact/hooks'; +import { useState } from 'preact/hooks'; import getAddressSummary from './utils/get-fastlane-address-summary'; import FastlaneSDK from '../../../../../src/components/PayPalFastlane/FastlaneSDK'; +import type { FastlaneShipping } from '../../../../../src/components/PayPalFastlane/types'; interface ShippingWithFastlaneProps { fastlaneSdk: FastlaneSDK; - address: any; - onCheckoutClick: () => void; + address: FastlaneShipping; + onCheckoutClick: (shippingAddress?: any) => void; } export const ShippingWithFastlane = ({ fastlaneSdk, address, onCheckoutClick }: ShippingWithFastlaneProps) => { - const [addressSummary, setAddressSummary] = useState(null); - - useEffect(() => { - const summary = getAddressSummary(address); - setAddressSummary(summary); - }, [address]); + const [addressSummary, setAddressSummary] = useState(getAddressSummary(address)); + const [shippingAddress, setShippingAddress] = useState(address); const handleShippingClick = async () => { const data = await fastlaneSdk.showShippingAddressSelector(); - console.log(data); if (data.selectionChanged) { const summary = getAddressSummary(data.selectedAddress); setAddressSummary(summary); + setShippingAddress(data.selectedAddress); } }; const handleCheckoutClick = () => { - onCheckoutClick(); + onCheckoutClick(shippingAddress); }; return ( -
+

Shipping Details

@@ -52,6 +49,6 @@ export const ShippingWithFastlane = ({ fastlaneSdk, address, onCheckoutClick }:
)} -
+ ); }; From 15f47ff3e4c7a7668fdf5af00fb6829ff38e6e25 Mon Sep 17 00:00:00 2001 From: guilhermer Date: Mon, 25 Nov 2024 12:20:03 +0100 Subject: [PATCH 3/8] cleaned up types --- .../components/PayPalFastlane/FastlaneSDK.ts | 2 +- .../services/request-fastlane-token.ts | 21 ++-- .../src/components/PayPalFastlane/types.ts | 112 ++++++++---------- .../{utils.ts => utils/convert-locale.ts} | 0 packages/lib/src/types/custom.d.ts | 4 +- .../Fastlane/components/CollectEmail.tsx | 11 +- 6 files changed, 73 insertions(+), 77 deletions(-) rename packages/lib/src/components/PayPalFastlane/{utils.ts => utils/convert-locale.ts} (100%) diff --git a/packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts b/packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts index 686a7b8191..ee384c5c3a 100644 --- a/packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts +++ b/packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts @@ -1,6 +1,6 @@ import { resolveEnvironments } from '../../core/Environment'; import requestFastlaneToken from './services/request-fastlane-token'; -import { convertAdyenLocaleToFastlaneLocale } from './utils'; +import { convertAdyenLocaleToFastlaneLocale } from './utils/convert-locale'; import Script from '../../utils/Script'; import type { Fastlane, FastlaneAuthenticatedCustomerResult, FastlaneShippingAddressSelectorResult } from './types'; diff --git a/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts b/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts index bf4d68d963..e272a5fd07 100644 --- a/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts +++ b/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts @@ -5,20 +5,23 @@ export interface FastlaneTokenData { clientId: string; value: string; expiresAt: string; + merchantId: string; } function requestFastlaneToken(url: string, clientKey: string): Promise { - // @ts-ignore ignore for now const path = `utility/v1/payPalFastlane/tokens?clientKey=${clientKey}`; + return httpPost({ loadingContext: url, path, errorLevel: 'fatal' }); - return Promise.resolve({ - id: '2747bd08-783a-45c6-902b-3efbda5497b7', - clientId: 'AXy9hIzWB6h_LjZUHjHmsbsiicSIbL4GKOrcgomEedVjduUinIU4C2llxkW5p0OG0zTNgviYFceaXEnj', - merchantId: 'C3UCKQHMW4948', - value: 'eyJraWQiOiJkMTA2ZTUwNjkzOWYxMWVlYjlkMTAyNDJhYzEyMDAwMiIsInR5cCI6IkpXVCIsImFsZyI6IkVTMjU2In0.eyJpc3MiOiJodHRwczovL2FwaS5zYW5kYm94LnBheXBhbC5jb20iLCJhdWQiOlsiaHR0cHM6Ly9hcGkuYnJhaW50cmVlZ2F0ZXdheS5jb20iLCJjaGVja291dC1wbGF5Z3JvdW5kLm5ldGxpZnkuYXBwIl0sInN1YiI6Ik02VE5BRVNaNUZHTk4iLCJhY3IiOlsiY2xpZW50Il0sInNjb3BlIjpbIkJyYWludHJlZTpWYXVsdCJdLCJvcHRpb25zIjp7fSwiYXoiOiJjY2cxOC5zbGMiLCJleHRlcm5hbF9pZCI6WyJQYXlQYWw6QzNVQ0tRSE1XNDk0OCIsIkJyYWludHJlZTozZGI4aG5rdHJ0bXpzMmd0Il0sImV4cCI6MTczMDk4ODQ5MSwiaWF0IjoxNzMwOTg3NTkxLCJqdGkiOiJVMkFBTHJkX3lOb3lMM2tqNzNYZndXTWtFdHBDdFA4aklJZkhtV1dMRFJ1UlYyR0U2M1A2b2RDNmZoTjF3Nmg1YUhQWFFUWFkzTzhuZG16ZmtuZmJJWC1zRGx0R2FRamt0RDd2cVVya2NNZkxlbEFIa1ZYSVptZkpNb0JVNWtRZyJ9.sRKQXrQjRn2CEOIeBwXsspd8Z7axRdMYjY95ga6mfYemcQrKZxBe3ASDCuWrCpbUvm04VTxXs80bR4V7hDAHoQ', - expiresAt: '2024-11-01T13:34:01.804+00:00' - }); - // return httpPost({ loadingContext: url, path, errorLevel: 'fatal' }); + /** + * TODO: Endpoint is not ready. The only way to test right now is mocking the response here + */ + // return Promise.resolve({ + // id: '2747bd08-783a-45c6-902b-3efbda5497b7', + // clientId: 'AXy9hIzWB6h_LjZUHjHmsbsiicSIbL4GKOrcgomEedVjduUinIU4C2llxkW5p0OG0zTNgviYFceaXEnj', + // merchantId: 'C3UCKQHMW4948', + // value: 'eyJraWQiOiJkMTA2ZTUwNjkzOWYxMWVlYjlkMTAyNDJhYzEyMDAwMiIsInR5cCI6IkpXVCIsImFsZyI6IkVTMjU2In0.eyJpc3MiOiJodHRwczovL2FwaS5zYW5kYm94LnBheXBhbC5jb20iLCJhdWQiOlsiaHR0cHM6Ly9hcGkuYnJhaW50cmVlZ2F0ZXdheS5jb20iLCJjaGVja291dC1wbGF5Z3JvdW5kLm5ldGxpZnkuYXBwIl0sInN1YiI6Ik02VE5BRVNaNUZHTk4iLCJhY3IiOlsiY2xpZW50Il0sInNjb3BlIjpbIkJyYWludHJlZTpWYXVsdCJdLCJvcHRpb25zIjp7fSwiYXoiOiJjY2cxOC5zbGMiLCJleHRlcm5hbF9pZCI6WyJQYXlQYWw6QzNVQ0tRSE1XNDk0OCIsIkJyYWludHJlZTozZGI4aG5rdHJ0bXpzMmd0Il0sImV4cCI6MTczMjUzMjkzNSwiaWF0IjoxNzMyNTMyMDM1LCJqdGkiOiJVMkFBTC16eVMxcXhaSjRxSWM5ei1wei0tODRGSkx1M2NDWFFKblZCb0tWemVoWmhTZzNHTVNCQW9WcHhoNmZFY2pNM3hqTnJWRzM2NTdVOUJCWHFjSlhRVW9ucjQ0dlZJcEZlaUZua1lmSmZHR3d0M1Z5ZW5jUWJVeHc1S0szQSJ9.3wp9h4NBcDqs8VhHFLRmQfEN-rFutInOCrRN0mYrkS1_fsqB2IZ6jWt0SjRhY16jPtbvyxnHa_6Cv7_1MVPlQQ', + // expiresAt: '2024-11-01T13:34:01.804+00:00' + // }); } export default requestFastlaneToken; diff --git a/packages/lib/src/components/PayPalFastlane/types.ts b/packages/lib/src/components/PayPalFastlane/types.ts index ca051478f9..725f9028e7 100644 --- a/packages/lib/src/components/PayPalFastlane/types.ts +++ b/packages/lib/src/components/PayPalFastlane/types.ts @@ -1,14 +1,11 @@ -export type FastlaneConstructor = (options: FastlaneOptions) => Promise; - /** * PayPal Fastlane Reference: * https://developer.paypal.com/docs/checkout/fastlane/reference/#link-customizeyourintegration */ -// TODO: Verify if we pass options here -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface FastlaneOptions {} - +/** + * Fastlane object available in the window + */ export interface Fastlane { identity: { lookupCustomerByEmail: (email: string) => Promise<{ customerContextId: string }>; @@ -16,64 +13,19 @@ export interface Fastlane { }; profile: { showShippingAddressSelector: () => Promise; - showCardSelector: () => ShowCardSelectorResult; }; setLocale: (locale: string) => void; - FastlaneWatermarkComponent: (options: { includeAdditionalInfo: boolean }) => Promise; + FastlaneWatermarkComponent: (options: { includeAdditionalInfo: boolean }) => Promise<{ + render: (container) => null; + }>; } -interface FastlaneWatermarkComponent { - render: (container) => null; -} +// TODO: TBD if this is needed +export interface FastlaneOptions {} -// TODO: fill this in after workshop -// eslint-disable-next-line @typescript-eslint/no-empty-interface +// TODO: TBD if this is needed interface AuthenticationFlowOptions {} -/** - * The AuthenticatedCustomerResult object type is returned from the identity.triggerAuthenticationFlow() call. - */ -interface FastlaneProfile { - name: Name; - shippingAddress: FastlaneShipping; - card: PaymentToken; -} - -interface Name { - firstName: string; - lastName: string; - fullName: string; -} - -interface Phone { - nationalNumber: string; - countryCode: string; -} - -export interface FastlaneAddress { - addressLine1: string; - addressLine2: string; - adminArea1: string; - adminArea2: string; - postalCode: string; - countryCode: string; - phone: Phone; -} - -export interface FastlaneShipping { - name: Name; - address: FastlaneAddress; - phoneNumber: Phone; -} - -interface PaymentToken { - id: string; - paymentSource: PaymentSource; -} -interface PaymentSource { - card: CardPaymentSource; -} - interface CardPaymentSource { brand: string; expiry: string; @@ -82,11 +34,6 @@ interface CardPaymentSource { billingAddress: FastlaneAddress; } -interface ShowCardSelectorResult { - selectionChanged: boolean; - selectedCard: PaymentToken; -} - /** * External types */ @@ -98,3 +45,44 @@ export interface FastlaneAuthenticatedCustomerResult { authenticationState: 'succeeded' | 'failed' | 'canceled' | 'not_found'; profileData: FastlaneProfile; } + +export interface FastlaneAddress { + addressLine1: string; + addressLine2: string; + adminArea1: string; + adminArea2: string; + postalCode: string; + countryCode: string; + phone: { + nationalNumber: string; + countryCode: string; + }; +} + +export interface FastlaneShipping { + name: { + firstName: string; + lastName: string; + fullName: string; + }; + address: FastlaneAddress; + phoneNumber: { + nationalNumber: string; + countryCode: string; + }; +} + +export interface FastlaneProfile { + name: { + firstName: string; + lastName: string; + fullName: string; + }; + shippingAddress: FastlaneShipping; + card: { + id: string; + paymentSource: { + card: CardPaymentSource; + }; + }; +} diff --git a/packages/lib/src/components/PayPalFastlane/utils.ts b/packages/lib/src/components/PayPalFastlane/utils/convert-locale.ts similarity index 100% rename from packages/lib/src/components/PayPalFastlane/utils.ts rename to packages/lib/src/components/PayPalFastlane/utils/convert-locale.ts diff --git a/packages/lib/src/types/custom.d.ts b/packages/lib/src/types/custom.d.ts index 84a7f964db..e6e953639d 100644 --- a/packages/lib/src/types/custom.d.ts +++ b/packages/lib/src/types/custom.d.ts @@ -1,4 +1,4 @@ -import type { FastlaneConstructor } from '../components/PayPalFastlane/types'; +import { Fastlane, FastlaneOptions } from '../components/PayPalFastlane/types'; declare module '*.scss' { const content: { [className: string]: string }; @@ -9,7 +9,7 @@ declare global { interface Window { ApplePaySession?: ApplePaySession; paypal?: { - Fastlane?: FastlaneConstructor; + Fastlane?: (options?: FastlaneOptions) => Promise; }; } } diff --git a/packages/lib/storybook/stories/wallets/Fastlane/components/CollectEmail.tsx b/packages/lib/storybook/stories/wallets/Fastlane/components/CollectEmail.tsx index 01ef0d0434..510f4019e6 100644 --- a/packages/lib/storybook/stories/wallets/Fastlane/components/CollectEmail.tsx +++ b/packages/lib/storybook/stories/wallets/Fastlane/components/CollectEmail.tsx @@ -28,9 +28,14 @@ export const CollectEmail = ({ fastlaneSdk, onFastlaneLookup, onEditEmail }: Col }; const handleButtonClick = async () => { - const authResult = await fastlaneSdk.authenticate(email); - onFastlaneLookup(authResult); - setViewOnly(true); + try { + const authResult = await fastlaneSdk.authenticate(email); + console.log('triggerAuthenticationFlow result:', authResult); + onFastlaneLookup(authResult); + setViewOnly(true); + } catch (error) { + console.log(error); + } }; useEffect(() => { From 31c71e37aa87c9c5d26bb326cbcf65a5e20c8323 Mon Sep 17 00:00:00 2001 From: guilhermer Date: Mon, 25 Nov 2024 13:37:33 +0100 Subject: [PATCH 4/8] fixed types --- packages/lib/src/types/custom.d.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/lib/src/types/custom.d.ts b/packages/lib/src/types/custom.d.ts index e6e953639d..5e6dd10980 100644 --- a/packages/lib/src/types/custom.d.ts +++ b/packages/lib/src/types/custom.d.ts @@ -11,13 +11,18 @@ declare global { paypal?: { Fastlane?: (options?: FastlaneOptions) => Promise; }; + AdyenWeb: any; + VISA_SDK?: { + buildClientProfile?(srciDpaId?: string): any; + correlationId?: string; + }; } } -interface Window { - AdyenWeb: any; - VISA_SDK?: { - buildClientProfile?(srciDpaId?: string): any; - correlationId?: string; - }; -} +// interface Window { +// AdyenWeb: any; +// VISA_SDK?: { +// buildClientProfile?(srciDpaId?: string): any; +// correlationId?: string; +// }; +// } From 4331406a13cf2c832b5eff56d9b828008ff76681 Mon Sep 17 00:00:00 2001 From: guilhermer Date: Mon, 25 Nov 2024 13:44:33 +0100 Subject: [PATCH 5/8] removed comment --- packages/lib/src/types/custom.d.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/lib/src/types/custom.d.ts b/packages/lib/src/types/custom.d.ts index 5e6dd10980..433bc511a5 100644 --- a/packages/lib/src/types/custom.d.ts +++ b/packages/lib/src/types/custom.d.ts @@ -18,11 +18,3 @@ declare global { }; } } - -// interface Window { -// AdyenWeb: any; -// VISA_SDK?: { -// buildClientProfile?(srciDpaId?: string): any; -// correlationId?: string; -// }; -// } From 7be420a1a1f9b856eea7307635537d10eb408ee3 Mon Sep 17 00:00:00 2001 From: guilhermer Date: Tue, 26 Nov 2024 15:24:15 +0100 Subject: [PATCH 6/8] fastlane wrapper unit tests --- .../PayPalFastlane/FastlaneSDK.test.ts | 148 ++++++++++++++++++ .../components/PayPalFastlane/FastlaneSDK.ts | 18 ++- .../services/request-fastlane-token.ts | 2 +- 3 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 packages/lib/src/components/PayPalFastlane/FastlaneSDK.test.ts diff --git a/packages/lib/src/components/PayPalFastlane/FastlaneSDK.test.ts b/packages/lib/src/components/PayPalFastlane/FastlaneSDK.test.ts new file mode 100644 index 0000000000..91a87259a4 --- /dev/null +++ b/packages/lib/src/components/PayPalFastlane/FastlaneSDK.test.ts @@ -0,0 +1,148 @@ +import { mockDeep, mock, mockReset } from 'jest-mock-extended'; +import initializeFastlane from './initializeFastlane'; +import { httpPost } from '../../core/Services/http'; +import Script from '../../utils/Script'; +import type { Fastlane, FastlaneProfile, FastlaneShipping } from './types'; + +const fastlaneMock = mockDeep(); +const fastlaneConstructorMock = jest.fn().mockResolvedValue(fastlaneMock); + +const mockScriptLoaded = jest.fn().mockImplementation(() => { + window.paypal = {}; + window.paypal.Fastlane = fastlaneConstructorMock; + return Promise.resolve(); +}); + +jest.mock('../../core/Services/http'); +jest.mock('../../utils/Script', () => { + return jest.fn().mockImplementation(() => { + return { load: mockScriptLoaded }; + }); +}); + +const httpPostMock = (httpPost as jest.Mock).mockResolvedValue({ + id: 'RANDOM-ID', + clientId: 'CLIENT-ID', + merchantId: 'XXXYYYZZZ', + value: 'TOKEN-VALUE', + expiresAt: '2024-11-01T13:34:01.804+00:00' +}); + +describe('FastlaneSDK', () => { + beforeEach(() => { + mockReset(fastlaneMock); + }); + + test('should initialize the Fastlane SDK', async () => { + await initializeFastlane({ + clientKey: 'test_xxx', + environment: 'test' + }); + + expect(fastlaneConstructorMock).toHaveBeenCalledTimes(1); + expect(fastlaneConstructorMock).toHaveBeenCalledWith({}); + expect(fastlaneMock.setLocale).toHaveBeenCalledWith('en_us'); + expect(httpPostMock).toHaveBeenCalledWith({ + loadingContext: 'https://checkoutshopper-test.adyen.com/checkoutshopper/', + path: 'utility/v1/payPalFastlane/tokens?clientKey=test_xxx', + errorLevel: 'fatal' + }); + expect(Script).toHaveBeenCalledWith( + 'https://www.paypal.com/sdk/js?client-id=CLIENT-ID&components=buttons,fastlane', + 'body', + {}, + { sdkClientToken: 'TOKEN-VALUE' } + ); + }); + + test('should return not_found if email is not recognized', async () => { + fastlaneMock.identity.lookupCustomerByEmail.mockResolvedValue({ + customerContextId: null + }); + + const fastlane = await initializeFastlane({ + clientKey: 'test_xxx', + environment: 'test' + }); + + const authResult = await fastlane.authenticate('test@adyen.com'); + + expect(authResult.authenticationState).toBe('not_found'); + expect(authResult.profileData).toBeUndefined(); + }); + + test('should authenticate the user with email', async () => { + const customerContextId = 'customer-context-id'; + const mockedFastlaneProfile = mock(); + + fastlaneMock.identity.lookupCustomerByEmail.mockResolvedValue({ + customerContextId + }); + + fastlaneMock.identity.triggerAuthenticationFlow.mockResolvedValue({ + authenticationState: 'succeeded', + profileData: mockedFastlaneProfile + }); + + const fastlane = await initializeFastlane({ + clientKey: 'test_xxx', + environment: 'test' + }); + + const authResult = await fastlane.authenticate('test@adyen.com'); + + expect(fastlaneMock.identity.lookupCustomerByEmail).toHaveBeenCalledWith('test@adyen.com'); + expect(fastlaneMock.identity.triggerAuthenticationFlow).toHaveBeenCalledWith(customerContextId); + expect(authResult.authenticationState).toBe('succeeded'); + expect(authResult.profileData).toBeDefined(); + }); + + test('should call Fastlane shipping address selector method', async () => { + const customerContextId = 'customer-context-id'; + const mockedFastlaneProfile = mock(); + const mockedFastlaneShipping = mock(); + + const fastlane = await initializeFastlane({ + clientKey: 'test_xxx', + environment: 'test' + }); + + fastlaneMock.profile.showShippingAddressSelector.mockResolvedValue({ + selectionChanged: false, + selectedAddress: mockedFastlaneShipping + }); + + fastlaneMock.identity.lookupCustomerByEmail.mockResolvedValue({ + customerContextId + }); + + fastlaneMock.identity.triggerAuthenticationFlow.mockResolvedValue({ + authenticationState: 'succeeded', + profileData: mockedFastlaneProfile + }); + + await fastlane.authenticate('test@adyen.com'); + const addressSelectorResult = await fastlane.showShippingAddressSelector(); + + expect(fastlaneMock.profile.showShippingAddressSelector).toHaveBeenCalledTimes(1); + expect(addressSelectorResult.selectionChanged).toBeFalsy(); + }); + + test('should mount Fastlane watermark', async () => { + const componentMock = { + render: jest.fn() + }; + fastlaneMock.FastlaneWatermarkComponent.mockResolvedValue(componentMock); + + const fastlane = await initializeFastlane({ + clientKey: 'test_xxx', + environment: 'test' + }); + + await fastlane.mountWatermark('.my-div'); + + expect(fastlaneMock.FastlaneWatermarkComponent).toHaveBeenCalledTimes(1); + expect(componentMock.render).toHaveBeenCalledTimes(1); + expect(componentMock.render).toHaveBeenCalledWith('.my-div'); + }); +}); diff --git a/packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts b/packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts index ee384c5c3a..5006eaac08 100644 --- a/packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts +++ b/packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts @@ -6,11 +6,12 @@ import Script from '../../utils/Script'; import type { Fastlane, FastlaneAuthenticatedCustomerResult, FastlaneShippingAddressSelectorResult } from './types'; import type { FastlaneTokenData } from './services/request-fastlane-token'; import type { CoreConfiguration } from '../../core/types'; +import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; export interface FastlaneSDKConfiguration { clientKey: string; + environment: CoreConfiguration['environment']; locale?: 'en-US' | 'es-US' | 'fr-RS' | 'zh-US'; - environment?: CoreConfiguration['environment']; } class FastlaneSDK { @@ -21,6 +22,9 @@ class FastlaneSDK { private fastlaneSdk: Fastlane; constructor(configuration: FastlaneSDKConfiguration) { + if (!configuration.environment) throw new AdyenCheckoutError('IMPLEMENTATION_ERROR', "FastlaneSDK: 'environment' property is required"); + if (!configuration.clientKey) throw new AdyenCheckoutError('IMPLEMENTATION_ERROR', "FastlaneSDK: 'clientKey' property is required"); + const { apiUrl } = resolveEnvironments(configuration.environment); this.checkoutShopperURL = apiUrl; @@ -36,6 +40,10 @@ class FastlaneSDK { } public async authenticate(email: string): Promise { + if (!this.fastlaneSdk) { + throw new AdyenCheckoutError('IMPLEMENTATION_ERROR', 'FastlaneSDK is not initialized'); + } + const { customerContextId } = await this.fastlaneSdk.identity.lookupCustomerByEmail(email); if (customerContextId) { @@ -49,11 +57,17 @@ class FastlaneSDK { } public showShippingAddressSelector(): Promise { - if (!this.fastlaneSdk.profile) return null; + if (!this.fastlaneSdk) { + throw new AdyenCheckoutError('IMPLEMENTATION_ERROR', 'FastlaneSDK is not initialized'); + } return this.fastlaneSdk.profile.showShippingAddressSelector(); } public async mountWatermark(container: HTMLElement | string, options = { includeAdditionalInfo: false }): Promise { + if (!this.fastlaneSdk) { + throw new AdyenCheckoutError('IMPLEMENTATION_ERROR', 'FastlaneSDK is not initialized'); + } + const component = await this.fastlaneSdk.FastlaneWatermarkComponent(options); component.render(container); } diff --git a/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts b/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts index e272a5fd07..ddebcf5914 100644 --- a/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts +++ b/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts @@ -19,7 +19,7 @@ function requestFastlaneToken(url: string, clientKey: string): Promise Date: Thu, 28 Nov 2024 15:02:20 +0100 Subject: [PATCH 7/8] returning component config --- .../PayPalFastlane/FastlaneSDK.test.ts | 81 +++++++++++++++++++ .../components/PayPalFastlane/FastlaneSDK.ts | 59 +++++++++++--- .../PayPalFastlane/initializeFastlane.ts | 2 +- .../services/request-fastlane-token.ts | 2 +- .../src/components/PayPalFastlane/types.ts | 35 ++++++++ .../Fastlane/FastlaneInSinglePageApp.tsx | 10 +-- .../Fastlane/components/GuestShopperForm.tsx | 19 +++-- 7 files changed, 183 insertions(+), 25 deletions(-) diff --git a/packages/lib/src/components/PayPalFastlane/FastlaneSDK.test.ts b/packages/lib/src/components/PayPalFastlane/FastlaneSDK.test.ts index 91a87259a4..d7824b383a 100644 --- a/packages/lib/src/components/PayPalFastlane/FastlaneSDK.test.ts +++ b/packages/lib/src/components/PayPalFastlane/FastlaneSDK.test.ts @@ -145,4 +145,85 @@ describe('FastlaneSDK', () => { expect(componentMock.render).toHaveBeenCalledTimes(1); expect(componentMock.render).toHaveBeenCalledWith('.my-div'); }); + + test('should return fastlane component configuration if shopper has profile', async () => { + const customerContextId = 'customer-context-id'; + fastlaneMock.identity.lookupCustomerByEmail.mockResolvedValue({ + customerContextId + }); + fastlaneMock.identity.triggerAuthenticationFlow.mockResolvedValue({ + authenticationState: 'succeeded', + profileData: mock({ + card: { + id: 'xxxx', + paymentSource: { + card: { + brand: 'visa', + lastDigits: '1111' + } + } + } + }) + }); + + const fastlane = await initializeFastlane({ + clientKey: 'test_xxx', + environment: 'test' + }); + const authResult = await fastlane.authenticate('test@adyen.com'); + const config = fastlane.getComponentConfiguration(authResult); + + expect(config).toStrictEqual({ + paymentType: 'fastlane', + configuration: { + brand: 'visa', + customerId: 'customer-context-id', + email: 'test@adyen.com', + lastFour: '1111', + sessionId: 'xxxx-yyyy', + tokenId: 'xxxx' + } + }); + }); + + test('should return card component configuration if shopper does not have profile', async () => { + const customerContextId = 'customer-context-id'; + fastlaneMock.identity.lookupCustomerByEmail.mockResolvedValue({ + customerContextId + }); + fastlaneMock.identity.triggerAuthenticationFlow.mockResolvedValue({ + authenticationState: 'not_found', + profileData: undefined + }); + + const fastlane = await initializeFastlane({ + clientKey: 'test_xxx', + environment: 'test' + }); + const authResult = await fastlane.authenticate('test@adyen.com'); + const config = fastlane.getComponentConfiguration(authResult); + + expect(config).toStrictEqual({ + paymentType: 'card', + configuration: { + fastlaneConfiguration: { + defaultToggleState: true, + privacyPolicyLink: 'https://...', + showConsent: true, + termsAndConditionsLink: 'https://...', + termsAndConditionsVersion: 'v1' + } + } + }); + }); + + test('should thrown an error if there is no auth result to create the component configuration', async () => { + const fastlane = await initializeFastlane({ + clientKey: 'test_xxx', + environment: 'test' + }); + + // @ts-ignore It is expected to omit the parameter here + expect(() => fastlane.getComponentConfiguration()).toThrowError(); + }); }); diff --git a/packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts b/packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts index 5006eaac08..464d6abfad 100644 --- a/packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts +++ b/packages/lib/src/components/PayPalFastlane/FastlaneSDK.ts @@ -2,17 +2,15 @@ import { resolveEnvironments } from '../../core/Environment'; import requestFastlaneToken from './services/request-fastlane-token'; import { convertAdyenLocaleToFastlaneLocale } from './utils/convert-locale'; import Script from '../../utils/Script'; - -import type { Fastlane, FastlaneAuthenticatedCustomerResult, FastlaneShippingAddressSelectorResult } from './types'; -import type { FastlaneTokenData } from './services/request-fastlane-token'; -import type { CoreConfiguration } from '../../core/types'; import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; - -export interface FastlaneSDKConfiguration { - clientKey: string; - environment: CoreConfiguration['environment']; - locale?: 'en-US' | 'es-US' | 'fr-RS' | 'zh-US'; -} +import { + ComponentConfiguration, + Fastlane, + FastlaneAuthenticatedCustomerResult, + FastlaneShippingAddressSelectorResult, + FastlaneSDKConfiguration +} from './types'; +import type { FastlaneTokenData } from './services/request-fastlane-token'; class FastlaneSDK { private readonly clientKey: string; @@ -20,6 +18,7 @@ class FastlaneSDK { private readonly locale: string; private fastlaneSdk: Fastlane; + private authenticatedShopper: { email: string; customerId: string }; constructor(configuration: FastlaneSDKConfiguration) { if (!configuration.environment) throw new AdyenCheckoutError('IMPLEMENTATION_ERROR', "FastlaneSDK: 'environment' property is required"); @@ -47,6 +46,7 @@ class FastlaneSDK { const { customerContextId } = await this.fastlaneSdk.identity.lookupCustomerByEmail(email); if (customerContextId) { + this.authenticatedShopper = { email, customerId: customerContextId }; return this.fastlaneSdk.identity.triggerAuthenticationFlow(customerContextId); } else { return { @@ -56,6 +56,45 @@ class FastlaneSDK { } } + /** + * TODO: Waiting for PayPal to provide the specific methods to fetch sessionId and Consent UI details + */ + public getComponentConfiguration(authResult: FastlaneAuthenticatedCustomerResult): ComponentConfiguration { + if (!authResult) { + throw new AdyenCheckoutError( + 'IMPLEMENTATION_ERROR', + 'FastlaneSDK: you must pass the authentication result to get the component configuration' + ); + } + + if (authResult.authenticationState === 'succeeded') { + return { + paymentType: 'fastlane', + configuration: { + sessionId: 'xxxx-yyyy', + customerId: this.authenticatedShopper.customerId, + email: this.authenticatedShopper.email, + tokenId: authResult.profileData.card.id, + lastFour: authResult.profileData.card.paymentSource.card.lastDigits, + brand: authResult.profileData.card.paymentSource.card.brand + } + }; + } else { + return { + paymentType: 'card', + configuration: { + fastlaneConfiguration: { + showConsent: true, + defaultToggleState: true, + termsAndConditionsLink: 'https://...', + privacyPolicyLink: 'https://...', + termsAndConditionsVersion: 'v1' + } + } + }; + } + } + public showShippingAddressSelector(): Promise { if (!this.fastlaneSdk) { throw new AdyenCheckoutError('IMPLEMENTATION_ERROR', 'FastlaneSDK is not initialized'); diff --git a/packages/lib/src/components/PayPalFastlane/initializeFastlane.ts b/packages/lib/src/components/PayPalFastlane/initializeFastlane.ts index abe77612a5..d70aaad4f4 100644 --- a/packages/lib/src/components/PayPalFastlane/initializeFastlane.ts +++ b/packages/lib/src/components/PayPalFastlane/initializeFastlane.ts @@ -1,5 +1,5 @@ import FastlaneSDK from './FastlaneSDK'; -import type { FastlaneSDKConfiguration } from './FastlaneSDK'; +import type { FastlaneSDKConfiguration } from './types'; async function initializeFastlane(configuration: FastlaneSDKConfiguration): Promise { const fastlane = new FastlaneSDK(configuration); diff --git a/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts b/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts index ddebcf5914..fa019dbb2d 100644 --- a/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts +++ b/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts @@ -19,7 +19,7 @@ function requestFastlaneToken(url: string, clientKey: string): Promise { - const [fastlaneData, setFastlaneData] = useState(null); + const [componentConfig, setComponentConfig] = useState(null); - const handleOnCheckoutStep = (fastlaneSdk, fastlaneData, shippingAddress) => { - console.log('FastlaneInSinglePageApp', fastlaneSdk, fastlaneData, shippingAddress); - setFastlaneData(fastlaneData); + const handleOnCheckoutStep = config => { + console.log('Component config:', config); + setComponentConfig(config); }; - if (!fastlaneData) { + if (!componentConfig) { return ; } diff --git a/packages/lib/storybook/stories/wallets/Fastlane/components/GuestShopperForm.tsx b/packages/lib/storybook/stories/wallets/Fastlane/components/GuestShopperForm.tsx index 6b5245824a..720e3b20fd 100644 --- a/packages/lib/storybook/stories/wallets/Fastlane/components/GuestShopperForm.tsx +++ b/packages/lib/storybook/stories/wallets/Fastlane/components/GuestShopperForm.tsx @@ -11,12 +11,12 @@ import FastlaneSDK from '../../../../../src/components/PayPalFastlane/FastlaneSD import type { FastlaneAuthenticatedCustomerResult } from '../../../../../src/components/PayPalFastlane/types'; interface GuestShopperFormProps { - onCheckoutStep(fastlane: FastlaneSDK, fastlaneData: any, shippingAddress: any): void; + onCheckoutStep(componentConfig): void; } export const GuestShopperForm = ({ onCheckoutStep }: GuestShopperFormProps) => { const [fastlane, setFastlane] = useState(null); - const [fastlaneLookupData, setFastlaneLookupData] = useState(null); + const [fastlaneAuthResult, setFastlaneAuthResult] = useState(null); const loadFastlane = async () => { const sdk = await initializeFastlane({ @@ -27,15 +27,18 @@ export const GuestShopperForm = ({ onCheckoutStep }: GuestShopperFormProps) => { }; const handleOnEditEmail = () => { - setFastlaneLookupData(null); + setFastlaneAuthResult(null); }; const handleFastlaneLookup = data => { - setFastlaneLookupData(data); + setFastlaneAuthResult(data); }; const handleOnCheckoutClick = (shippingAddress?: any) => { - onCheckoutStep(fastlane, fastlaneLookupData, shippingAddress); + console.log('Shipping address', shippingAddress); + + const componentConfig = fastlane.getComponentConfiguration(fastlaneAuthResult); + onCheckoutStep(componentConfig); }; useEffect(() => { @@ -54,15 +57,15 @@ export const GuestShopperForm = ({ onCheckoutStep }: GuestShopperFormProps) => {
- {fastlaneLookupData?.authenticationState === 'succeeded' && ( + {fastlaneAuthResult?.authenticationState === 'succeeded' && ( )} - {fastlaneLookupData?.authenticationState === 'not_found' && } + {fastlaneAuthResult?.authenticationState === 'not_found' && }
); }; From 300c8b67d9430e3b5c63ce26d964375bd3eaa56b Mon Sep 17 00:00:00 2001 From: guilhermer Date: Fri, 29 Nov 2024 09:35:12 +0100 Subject: [PATCH 8/8] random commit --- .../PayPalFastlane/services/request-fastlane-token.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts b/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts index fa019dbb2d..dce2802b61 100644 --- a/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts +++ b/packages/lib/src/components/PayPalFastlane/services/request-fastlane-token.ts @@ -19,7 +19,7 @@ function requestFastlaneToken(url: string, clientKey: string): Promise