From 6a328b1b494edc303bb0644cd8857c5cbad2fbe7 Mon Sep 17 00:00:00 2001 From: Craig Warkentin Date: Mon, 16 Sep 2024 17:50:07 -0500 Subject: [PATCH] CHK-4580 ppcp google pay --- src/initialize/initialize.ts | 7 +- src/paypal/index.ts | 1 + src/paypal/initPpcp.ts | 6 + src/paypal/managePaypalState.ts | 68 ++++++++++- src/paypal/ppcp_buttons/initPpcpButtons.ts | 2 +- src/paypal/ppcp_google/createPPCPGoogle.ts | 40 +++++++ ...formatGooglePayContactToCheckoutAddress.ts | 27 +++++ .../getPPCPShippingOptionsGoogle.ts | 25 ++++ src/paypal/ppcp_google/index.ts | 8 ++ src/paypal/ppcp_google/initPPCPGoogle.ts | 32 ++++++ src/paypal/ppcp_google/ppcpOnClickGoogle.ts | 45 ++++++++ src/paypal/ppcp_google/ppcpOnLoadGoogle.ts | 53 +++++++++ .../ppcpOnPaymentAuthorizedGoogle.ts | 108 ++++++++++++++++++ .../ppcpOnPaymentDataChangeGoogle.ts | 82 +++++++++++++ src/types/paypal.ts | 39 +++++++ src/types/variables.ts | 28 ++++- src/variables/variables.ts | 19 ++- 17 files changed, 579 insertions(+), 11 deletions(-) create mode 100644 src/paypal/ppcp_google/createPPCPGoogle.ts create mode 100644 src/paypal/ppcp_google/formatGooglePayContactToCheckoutAddress.ts create mode 100644 src/paypal/ppcp_google/getPPCPShippingOptionsGoogle.ts create mode 100644 src/paypal/ppcp_google/index.ts create mode 100644 src/paypal/ppcp_google/initPPCPGoogle.ts create mode 100644 src/paypal/ppcp_google/ppcpOnClickGoogle.ts create mode 100644 src/paypal/ppcp_google/ppcpOnLoadGoogle.ts create mode 100644 src/paypal/ppcp_google/ppcpOnPaymentAuthorizedGoogle.ts create mode 100644 src/paypal/ppcp_google/ppcpOnPaymentDataChangeGoogle.ts diff --git a/src/initialize/initialize.ts b/src/initialize/initialize.ts index 154a5b9..4a0c9ba 100644 --- a/src/initialize/initialize.ts +++ b/src/initialize/initialize.ts @@ -17,6 +17,7 @@ import { initPPCPApple, initPpcp, } from 'src'; +import {initPPCPGoogle} from 'src/paypal/ppcp_google'; export function initialize(props: IInitializeProps): void{ let {alternative_payment_methods} = getOrderInitialData(); @@ -27,7 +28,8 @@ export function initialize(props: IInitializeProps): void{ const loadsPpcp = alternative_payment_methods.some(payment => payment.type === alternatePaymentMethodType.PPCP); if (loadsPpcp) { alternative_payment_methods = alternative_payment_methods - .filter(m => m.type !== alternatePaymentMethodType.PPCP_APPLE); + .filter(m => m.type !== alternatePaymentMethodType.PPCP_APPLE + || m.type !== alternatePaymentMethodType.PPCP_GOOGLE); } for (const paymentMethod of alternative_payment_methods) { @@ -45,9 +47,6 @@ export function initialize(props: IInitializeProps): void{ case alternatePaymentMethodType.BRAINTREE_APPLE: initBraintreeApple(paymentMethod as IExpressPayBraintreeApple); break; - case alternatePaymentMethodType.PPCP_APPLE: - initPPCPApple(paymentMethod as IExpressPayPaypalCommercePlatform); - break; case alternatePaymentMethodType.PPCP: initPpcp(props.onAction, !!props.fastlane); break; diff --git a/src/paypal/index.ts b/src/paypal/index.ts index 3c7ec67..fea6fe7 100644 --- a/src/paypal/index.ts +++ b/src/paypal/index.ts @@ -19,4 +19,5 @@ export * from './paypalOnClick'; export * from './paypalOnload'; export * from './paypalOnShippingChange'; export * from './ppcp_apple'; +export * from './ppcp_google'; export * from './ppcp_buttons'; diff --git a/src/paypal/initPpcp.ts b/src/paypal/initPpcp.ts index 2035276..f8b4401 100644 --- a/src/paypal/initPpcp.ts +++ b/src/paypal/initPpcp.ts @@ -13,6 +13,8 @@ import { setOnAction, showPaymentMethodTypes } from 'src'; +import {initPPCPGoogle} from 'src/paypal/ppcp_google'; +import {IExpressPayBraintreeGoogle} from '@boldcommerce/checkout-frontend-library/lib/types/apiInterfaces'; export async function initPpcp(callback?: IOnAction, fastlane = false): Promise { @@ -28,6 +30,10 @@ export async function initPpcp(callback?: IOnAction, fastlane = false): Promise< if(applePayment){ await initPPCPApple(applePayment as IExpressPayPaypalCommercePlatform); } + const ppcpPayment = payment as IExpressPayBraintreeGoogle; + if(ppcpPayment.google_pay_enabled) { + await initPPCPGoogle(payment as IExpressPayPaypalCommercePlatform); + } } else { displayError('There was an unknown error while loading the paypal buttons. Please try again.', 'generic', 'unknown_error'); enableDisableSection( showPaymentMethodTypes.PPCP, true); diff --git a/src/paypal/managePaypalState.ts b/src/paypal/managePaypalState.ts index 7dd560a..395c5a8 100644 --- a/src/paypal/managePaypalState.ts +++ b/src/paypal/managePaypalState.ts @@ -5,10 +5,11 @@ import { IPaypalNamespaceApple, IPPCPAppleConfig, IPPCPApplePayInstance, - paypalState + paypalState, IPaypalNamespaceGoogle, IPPCPGooglePayInstance, IPPCPGoogleConfig } from 'src'; +import PaymentsClient = google.payments.api.PaymentsClient; -export function setPaypalNameSpace(paypal: PayPalNamespace | IPaypalNamespaceApple | null): void { +export function setPaypalNameSpace(paypal: PayPalNamespace | IPaypalNamespaceApple | IPaypalNamespaceGoogle | null): void { paypalState.paypal = paypal; } @@ -20,7 +21,7 @@ export function getPaypalNameSpacePromise(): Promise | n return paypalState.paypalPromise; } -export function getPaypalNameSpace(): PayPalNamespace | IPaypalNamespaceApple | null { +export function getPaypalNameSpace(): PayPalNamespace | IPaypalNamespaceApple | IPaypalNamespaceGoogle | null { return paypalState.paypal; } @@ -33,6 +34,11 @@ export function hasPaypalNameSpaceApple(): boolean { return !!paypal && !!paypal.Applepay; } +export function hasPaypalNameSpaceGoogle(): boolean { + const paypal = paypalState.paypal as IPaypalNamespaceGoogle; + return !!paypal && !!paypal.Googlepay; +} + export function setPPCPApplePayInstance(ppcpApplePayInstance: IPPCPApplePayInstance | null): void { paypalState.ppcpApplePayInstance = ppcpApplePayInstance; } @@ -77,6 +83,51 @@ export function getPPCPApplePaySessionChecked(): ApplePaySession { } return paypalState.ppcpApplePaySession; } +export function setPPCPGooglePayInstance(ppcpGooglePayInstance: IPPCPGooglePayInstance | null): void { + paypalState.ppcpGooglePayInstance = ppcpGooglePayInstance; +} + +export function getPPCPGooglePayInstance(): IPPCPGooglePayInstance | null { + return paypalState.ppcpGooglePayInstance; +} + +export function getPPCPGooglePayInstanceChecked(): IPPCPGooglePayInstance { + if (!paypalState.ppcpGooglePayInstance) { + throw new PaypalNullStateKeyError('Precondition violated: ppcpGooglePayInstance is null'); + } + return paypalState.ppcpGooglePayInstance; +} + +export function setPPCPGooglePayConfig(ppcpGooglePayConfig: IPPCPGoogleConfig | null): void { + paypalState.ppcpGooglePayConfig = ppcpGooglePayConfig; +} + +export function getPPCPGooglePayConfig(): IPPCPGoogleConfig | null { + return paypalState.ppcpGooglePayConfig; +} + +export function getPPCPGooglePayConfigChecked(): IPPCPGoogleConfig { + if (!paypalState.ppcpGooglePayConfig) { + throw new PaypalNullStateKeyError('Precondition violated: ppcpGooglePayConfig is null'); + } + return paypalState.ppcpGooglePayConfig; +} + +export function setPPCPGooglePaySession(ppcpGooglePaySession: PaymentsClient | null): void { + paypalState.ppcpGooglePaySession = ppcpGooglePaySession; +} + +export function getPPCPGooglePaySession(): PaymentsClient | null { + return paypalState.ppcpGooglePaySession; +} + +export function getPPCPGooglePaySessionChecked(): PaymentsClient { + if (!paypalState.ppcpGooglePaySession) { + throw new PaypalNullStateKeyError('Precondition violated: ppcpGooglePaySession is null'); + } + return paypalState.ppcpGooglePaySession; +} + export function setPaypalGatewayPublicId(gatewayPublicId: string): void { paypalState.gatewayPublicId = gatewayPublicId; @@ -96,3 +147,14 @@ export function getPPCPAppleCredentialsChecked(): IExpressPayPaypalCommercePlatf } return paypalState.ppcpAppleCredentials; } + +export function setPPCPGoogleCredentials(credentials: IExpressPayPaypalCommercePlatform | null): void { + paypalState.ppcpGoogleCredentials = credentials; +} + +export function getPPCPGoogleCredentialsChecked(): IExpressPayPaypalCommercePlatform { + if (!paypalState.ppcpGoogleCredentials) { + throw new PaypalNullStateKeyError('Precondition violated: ppcpGoogleCredentials is null'); + } + return paypalState.ppcpGoogleCredentials; +} \ No newline at end of file diff --git a/src/paypal/ppcp_buttons/initPpcpButtons.ts b/src/paypal/ppcp_buttons/initPpcpButtons.ts index 72815f0..27d755b 100644 --- a/src/paypal/ppcp_buttons/initPpcpButtons.ts +++ b/src/paypal/ppcp_buttons/initPpcpButtons.ts @@ -13,7 +13,7 @@ import {loadScript, PayPalNamespace} from '@paypal/paypal-js'; import {getCurrency,getEnvironment,getJwtToken,getPublicOrderId,getShopIdentifier,IExpressPayPaypalCommercePlatformButton} from '@boldcommerce/checkout-frontend-library'; async function initPpcpSdkInternal(payment: IExpressPayPaypalCommercePlatformButton, /* istanbul ignore next */ fastlane = false): Promise { - let components = `buttons,applepay${fastlane ? ',fastlane' : ''}`; + let components = `buttons,applepay,googlepay${fastlane ? ',fastlane' : ''}`; const {iso_code: currency} = getCurrency(); const merchantCountry = payment.merchant_country; let buyerCountry = ''; diff --git a/src/paypal/ppcp_google/createPPCPGoogle.ts b/src/paypal/ppcp_google/createPPCPGoogle.ts new file mode 100644 index 0000000..c0aae96 --- /dev/null +++ b/src/paypal/ppcp_google/createPPCPGoogle.ts @@ -0,0 +1,40 @@ +import { + enableDisableSection, getPPCPGooglePayConfigChecked, + getPPCPGooglePaySession, + showPaymentMethodTypes, + ppcpOnClickGoogle +} from 'src'; + +export function createPPCPGoogle(): void { + + const isMounted = !!document.getElementById('ppcp-apple-express-payment'); + const paymentsClient = getPPCPGooglePaySession(); + const {allowedPaymentMethods} = getPPCPGooglePayConfigChecked(); + + if (!isMounted) { + const ppcpGoogleDiv = document.createElement('div'); + ppcpGoogleDiv.id = 'ppcp-google-express-payment'; + ppcpGoogleDiv.className = 'ppcp-google-express-payment express-payment'; + ppcpGoogleDiv.dataset.testid = 'ppcp-google-express-payment'; + + const button = paymentsClient?.createButton({ + onClick: ppcpOnClickGoogle, + buttonType: 'short', + buttonSizeMode: 'fill', + allowedPaymentMethods: allowedPaymentMethods + }) as HTMLButtonElement; + + button.className = 'ppcp-google-pay-button'; + button.id = 'ppcp-google-pay-button'; + button.dataset.testid = 'ppcp-google-pay-button'; + ppcpGoogleDiv.appendChild(button); + + const container = document.getElementById('express-payment-container'); + if (!container) { + enableDisableSection( showPaymentMethodTypes.PPCP_GOOGLE, false); + return; + } + container.appendChild(ppcpGoogleDiv); + } + enableDisableSection( showPaymentMethodTypes.PPCP_GOOGLE, true); +} diff --git a/src/paypal/ppcp_google/formatGooglePayContactToCheckoutAddress.ts b/src/paypal/ppcp_google/formatGooglePayContactToCheckoutAddress.ts new file mode 100644 index 0000000..0a6fd63 --- /dev/null +++ b/src/paypal/ppcp_google/formatGooglePayContactToCheckoutAddress.ts @@ -0,0 +1,27 @@ +import {IAddress} from '@boldcommerce/checkout-frontend-library'; +import {getCountryName, getFirstAndLastName, getPhoneNumber, getProvinceDetails} from 'src'; +import Address = google.payments.api.Address; + +export function formatGooglePayContactToCheckoutAddress(address: Address | undefined, phoneNumberOverwrite = false): IAddress { + const {administrativeArea, countryCode, postalCode, locality, name, address1, address2, phoneNumber} = address ?? {}; + const {firstName, lastName} = getFirstAndLastName(name); + const countryIso = countryCode ?? ''; + const region = administrativeArea ?? ''; + const {code: provinceCode, name: provinceName} = getProvinceDetails(countryIso, region); + const countryName = getCountryName(countryIso); + + return { + 'first_name': firstName, + 'last_name': lastName, + 'address_line_1': address1 ?? '', + 'address_line_2': address2 ?? '', + 'country': countryName, + 'city': locality ?? '', + 'province':provinceName, + 'country_code': countryIso, + 'province_code': provinceCode, + 'postal_code': postalCode ?? '', + 'business_name': '', + 'phone_number': phoneNumber || (phoneNumberOverwrite ? getPhoneNumber(phoneNumber) : '') + }; +} diff --git a/src/paypal/ppcp_google/getPPCPShippingOptionsGoogle.ts b/src/paypal/ppcp_google/getPPCPShippingOptionsGoogle.ts new file mode 100644 index 0000000..ea047b4 --- /dev/null +++ b/src/paypal/ppcp_google/getPPCPShippingOptionsGoogle.ts @@ -0,0 +1,25 @@ +import {getValueByCurrency} from 'src'; +import {getCurrency, getShipping} from '@boldcommerce/checkout-frontend-library'; +import ShippingOptionParameters = google.payments.api.ShippingOptionParameters; +import SelectionOption = google.payments.api.SelectionOption; + +export function getPPCPShippingOptionsGoogle(): ShippingOptionParameters | undefined { + const {iso_code: currencyCode} = getCurrency(); + const {available_shipping_lines: shippingLines, selected_shipping: selectedShipping} = getShipping(); + const defaultSelectedOptionId = selectedShipping?.id; + + if (!shippingLines || (Array.isArray(shippingLines) && shippingLines.length < 1)) { + return; + } + + const shippingOptions = shippingLines.map(p => ({ + id: p.id, + label: `${getValueByCurrency(p.amount, currencyCode)}: ${p.description} `, + description: '' + })) as Array; + + return { + shippingOptions, + defaultSelectedOptionId + }; +} diff --git a/src/paypal/ppcp_google/index.ts b/src/paypal/ppcp_google/index.ts new file mode 100644 index 0000000..b9fce93 --- /dev/null +++ b/src/paypal/ppcp_google/index.ts @@ -0,0 +1,8 @@ +export * from './createPPCPGoogle'; +export * from './initPPCPGoogle'; +export * from './ppcpOnClickGoogle'; +export * from './ppcpOnLoadGoogle'; +export * from './ppcpOnPaymentAuthorizedGoogle'; +export * from './ppcpOnPaymentDataChangeGoogle'; +export * from './formatGooglePayContactToCheckoutAddress'; +export * from './getPPCPShippingOptionsGoogle'; diff --git a/src/paypal/ppcp_google/initPPCPGoogle.ts b/src/paypal/ppcp_google/initPPCPGoogle.ts new file mode 100644 index 0000000..0aad356 --- /dev/null +++ b/src/paypal/ppcp_google/initPPCPGoogle.ts @@ -0,0 +1,32 @@ +import {IExpressPayPaypalCommercePlatform} from '@boldcommerce/checkout-frontend-library'; +import {loadScript} from '@paypal/paypal-js'; +import {PayPalScriptOptions} from '@paypal/paypal-js/types/script-options'; +import { + getPaypalScriptOptions, + hasPaypalNameSpaceGoogle, + loadJS, + paypalConstants, + setPaypalNameSpace, + setPPCPGoogleCredentials, + ppcpOnLoadGoogle +} from 'src'; + +export async function initPPCPGoogle(payment: IExpressPayPaypalCommercePlatform): Promise { + setPPCPGoogleCredentials(payment); + + if (!hasPaypalNameSpaceGoogle()) { + const components = payment.google_pay_enabled ? 'googlepay' : undefined; + const paypalScriptOptions: PayPalScriptOptions = getPaypalScriptOptions(payment.partner_id, payment.is_test, payment.merchant_id, components); + + await loadJS(paypalConstants.GOOGLEPAY_JS); + const paypal = await loadScript(paypalScriptOptions); + + setPaypalNameSpace(paypal); + + if (paypal) { + ppcpOnLoadGoogle(); + } + } else { + await loadJS(paypalConstants.GOOGLEPAY_JS, ppcpOnLoadGoogle); + } +} diff --git a/src/paypal/ppcp_google/ppcpOnClickGoogle.ts b/src/paypal/ppcp_google/ppcpOnClickGoogle.ts new file mode 100644 index 0000000..d1eb04a --- /dev/null +++ b/src/paypal/ppcp_google/ppcpOnClickGoogle.ts @@ -0,0 +1,45 @@ +import {getCurrency, getOrderInitialData} from '@boldcommerce/checkout-frontend-library'; +import { + getPPCPGooglePayConfigChecked, getPPCPGooglePaySession, +} from 'src'; +import {getTotals, getValueByCurrency} from 'src/utils'; +import PaymentDataRequest = google.payments.api.PaymentDataRequest; + +export function ppcpOnClickGoogle(): void { + const googlePayConfig = getPPCPGooglePayConfigChecked(); + const {allowedPaymentMethods,merchantInfo, apiVersion, apiVersionMinor , countryCode} = googlePayConfig; + + const {iso_code: currencyCode} = getCurrency(); + const {totalAmountDue} = getTotals(); + const {general_settings: {checkout_process: {phone_number_required: isPhoneRequired}}} = getOrderInitialData(); + const {country_info: countryInfo} = getOrderInitialData(); + const allowedShippingCountries = countryInfo.filter(c => c.valid_for_shipping); + const allowedCountryCodes = allowedShippingCountries.map(c => c.iso_code.toUpperCase()); + const paymentMethod = allowedPaymentMethods[0]; + paymentMethod.parameters.billingAddressRequired = true; + paymentMethod.parameters.billingAddressParameters = {format: 'FULL', phoneNumberRequired: isPhoneRequired}; + + const paymentRequest: PaymentDataRequest = { + transactionInfo: { + currencyCode: currencyCode, + countryCode: countryCode, + totalPrice: getValueByCurrency(totalAmountDue, currencyCode), + totalPriceStatus: 'ESTIMATED', + }, + shippingAddressRequired: true, + emailRequired: true, + shippingAddressParameters: { + allowedCountryCodes: allowedCountryCodes, + phoneNumberRequired: isPhoneRequired, + }, + merchantInfo: merchantInfo, + apiVersion: apiVersion, + apiVersionMinor: apiVersionMinor, + allowedPaymentMethods: allowedPaymentMethods, + shippingOptionRequired: true, + callbackIntents: ['SHIPPING_ADDRESS', 'SHIPPING_OPTION', 'PAYMENT_AUTHORIZATION'], + }; + + const paymentsClient = getPPCPGooglePaySession(); + paymentsClient?.loadPaymentData(paymentRequest); +} diff --git a/src/paypal/ppcp_google/ppcpOnLoadGoogle.ts b/src/paypal/ppcp_google/ppcpOnLoadGoogle.ts new file mode 100644 index 0000000..595df3a --- /dev/null +++ b/src/paypal/ppcp_google/ppcpOnLoadGoogle.ts @@ -0,0 +1,53 @@ +import { + getPaypalNameSpace, + getPPCPGoogleCredentialsChecked, + GooglePayLoadingError, + hasPaypalNameSpaceGoogle, + IPaypalNamespaceGoogle, + setPPCPGooglePayConfig, + setPPCPGooglePaySession, + setPPCPGooglePayInstance, + createPPCPGoogle, + ppcpOnPaymentAuthorizedGoogle, + ppcpOnPaymentDataChangeGoogle +} from 'src'; + +export async function ppcpOnLoadGoogle(): Promise { + if (hasPaypalNameSpaceGoogle()) { + const paypal = getPaypalNameSpace() as IPaypalNamespaceGoogle; + + try { + const googlePay = paypal.Googlepay(); + const googlePayConfig = await googlePay.config(); + const is_test = getPPCPGoogleCredentialsChecked(); + setPPCPGooglePayInstance(googlePay); + setPPCPGooglePayConfig(googlePayConfig); + + const { allowedPaymentMethods, apiVersion, apiVersionMinor } = googlePayConfig; + + const googlePaySession = new google.payments.api.PaymentsClient({ + environment: is_test ? 'TEST' : 'PRODUCTION', + paymentDataCallbacks: { + onPaymentAuthorized: ppcpOnPaymentAuthorizedGoogle, + onPaymentDataChanged: ppcpOnPaymentDataChangeGoogle + } + }); + + setPPCPGooglePaySession(googlePaySession); + googlePaySession.isReadyToPay({allowedPaymentMethods, apiVersion,apiVersionMinor}) + .then(function(response) { + if (response.result) { + createPPCPGoogle(); + } + }); + + } catch (error) { + if (error instanceof Error) { + error.name = GooglePayLoadingError.name; + throw error; + } + + throw new GooglePayLoadingError(`Error loading Google Pay: ${error}`); + } + } +} diff --git a/src/paypal/ppcp_google/ppcpOnPaymentAuthorizedGoogle.ts b/src/paypal/ppcp_google/ppcpOnPaymentAuthorizedGoogle.ts new file mode 100644 index 0000000..eb86265 --- /dev/null +++ b/src/paypal/ppcp_google/ppcpOnPaymentAuthorizedGoogle.ts @@ -0,0 +1,108 @@ +import { + callBillingAddressEndpoint, + callGuestCustomerEndpoint, + callShippingAddressEndpoint, + orderProcessing, + getTotals, + isObjectEquals, + googlePayConstants, + getPPCPGoogleCredentialsChecked, getPaypalNameSpace, IPaypalNamespaceGoogle, + formatGooglePayContactToCheckoutAddress +} from 'src'; +import {API_RETRY} from 'src/types'; +import { + addPayment, + getCurrency, + IAddPaymentRequest, IAddPaymentResponse, IApiSuccessResponse, + setTaxes +} from '@boldcommerce/checkout-frontend-library'; +import PaymentData = google.payments.api.PaymentData; +import PaymentAuthorizationResult = google.payments.api.PaymentAuthorizationResult; +import PaymentMethodData = google.payments.api.PaymentMethodData; +import CardInfo = google.payments.api.CardInfo; +import PaymentDataError = google.payments.api.PaymentDataError; + +export async function ppcpOnPaymentAuthorizedGoogle(paymentData: PaymentData): Promise { + const {email, shippingAddress, paymentMethodData} = paymentData; + const {info, description} = paymentMethodData as PaymentMethodData; + const {billingAddress} = info as CardInfo; + const error: PaymentDataError = { + reason: googlePayConstants.GOOGLEPAY_ERROR_REASON_PAYMENT, + intent: googlePayConstants.GOOGLEPAY_INTENT_PAYMENT_AUTHORIZATION, + message: '' + }; + + const formattedShippingAddress = formatGooglePayContactToCheckoutAddress(shippingAddress); + const formattedBillingAddress = formatGooglePayContactToCheckoutAddress(billingAddress); + const isSameAddress = isObjectEquals(formattedShippingAddress, formattedBillingAddress); + + const customerResult = await callGuestCustomerEndpoint(formattedBillingAddress.first_name, formattedBillingAddress.last_name, email ?? ''); + if (!customerResult.success) { + error.message = customerResult.error?.message ?? 'There was an unknown error while validating your customer information.'; + return { + transactionState: googlePayConstants.GOOGLEPAY_TRANSACTION_STATE_ERROR, + error + }; + } + const shippingAddressResponse = await callShippingAddressEndpoint(formattedShippingAddress, true); + if (!shippingAddressResponse.success) { + error.reason = googlePayConstants.GOOGLEPAY_ERROR_REASON_SHIPPING; + error.message = shippingAddressResponse.error?.message ?? 'There was an unknown error while validating your shipping address.'; + return { + transactionState: googlePayConstants.GOOGLEPAY_TRANSACTION_STATE_ERROR, + error + }; + } + const billingAddressResponse = await callBillingAddressEndpoint(formattedBillingAddress, !isSameAddress); + if (!billingAddressResponse.success) { + error.message = billingAddressResponse.error?.message ?? 'There was an unknown error while validating your billing address.'; + return { + transactionState: googlePayConstants.GOOGLEPAY_TRANSACTION_STATE_ERROR, + error + }; + } + const taxResponse = await setTaxes(API_RETRY); + if (!taxResponse.success) { + error.message = taxResponse.error?.message ?? 'There was an unknown error while calculating the taxes.'; + return { + transactionState: googlePayConstants.GOOGLEPAY_TRANSACTION_STATE_ERROR, + error + }; + } + + const {public_id: gatewayPublicId} = getPPCPGoogleCredentialsChecked(); + const {iso_code: currencyCode} = getCurrency(); + const {totalAmountDue} = getTotals(); + const payment: IAddPaymentRequest = { + token: JSON.stringify(paymentData.paymentMethodData), + gateway_public_id: gatewayPublicId, + currency: currencyCode, + amount: totalAmountDue, + display_string: description, + wallet_pay_type: 'paywithgoogle', + extra_payment_data: { + brand: paymentData.paymentMethodData.info?.cardNetwork, + last_digits: paymentData.paymentMethodData.info?.cardDetails, + paymentSource: 'google_pay', + language: navigator.language, + } + }; + const paymentResult = await addPayment(payment, API_RETRY); + if (!paymentResult.success) { + error.message = paymentResult.error?.message ?? 'There was an unknown error while processing your payment.'; + return { + transactionState: googlePayConstants.GOOGLEPAY_TRANSACTION_STATE_ERROR, + error + }; + } else { + const successResponse = paymentResult.response as IApiSuccessResponse; + const {payment: addedPayment} = successResponse.data as IAddPaymentResponse; + const paypal = getPaypalNameSpace() as IPaypalNamespaceGoogle; + await paypal.Googlepay().confirmOrder({ + orderId: addedPayment.token, + paymentMethodData: paymentData.paymentMethodData + }); + } + orderProcessing(); + return {transactionState: googlePayConstants.GOOGLEPAY_TRANSACTION_STATE_SUCCESS}; +} diff --git a/src/paypal/ppcp_google/ppcpOnPaymentDataChangeGoogle.ts b/src/paypal/ppcp_google/ppcpOnPaymentDataChangeGoogle.ts new file mode 100644 index 0000000..7e4831d --- /dev/null +++ b/src/paypal/ppcp_google/ppcpOnPaymentDataChangeGoogle.ts @@ -0,0 +1,82 @@ +import { + API_RETRY, BRAINTREE_GOOGLE_EMPTY_SHIPPING_OPTION, + callShippingAddressEndpoint, getPPCPGooglePayConfigChecked, + getTotals, + getValueByCurrency, googlePayConstants, + formatGooglePayContactToCheckoutAddress, getPPCPShippingOptionsGoogle +} from 'src'; +import { + getCurrency, + getShipping, + getShippingLines, + setTaxes, + estimateShippingLines, + estimateTaxes, + getOrderInitialData, + changeShippingLine +} from '@boldcommerce/checkout-frontend-library'; +import IntermediatePaymentData = google.payments.api.IntermediatePaymentData; +import PaymentDataRequestUpdate = google.payments.api.PaymentDataRequestUpdate; +import CallbackIntent = google.payments.api.CallbackIntent; + +export async function ppcpOnPaymentDataChangeGoogle(intermediatePaymentData: IntermediatePaymentData): Promise { + const {countryCode} = getPPCPGooglePayConfigChecked(); + const {callbackTrigger, shippingAddress, shippingOptionData} = intermediatePaymentData; + const paymentDataRequestUpdate: PaymentDataRequestUpdate = {}; + const intent = callbackTrigger === googlePayConstants.GOOGLEPAY_TRIGGER_INITIALIZE ? googlePayConstants.GOOGLEPAY_INTENT_SHIPPING_ADDRESS : callbackTrigger as CallbackIntent; + const {general_settings} = getOrderInitialData(); + const rsaEnabled = general_settings.checkout_process.rsa_enabled; + + switch (callbackTrigger) { + case googlePayConstants.GOOGLEPAY_TRIGGER_INITIALIZE: + case googlePayConstants.GOOGLEPAY_INTENT_SHIPPING_OPTION: + case googlePayConstants.GOOGLEPAY_INTENT_SHIPPING_ADDRESS: { + let shippingLinesResponse; + const address = formatGooglePayContactToCheckoutAddress(shippingAddress, true); + if (rsaEnabled) { + shippingLinesResponse = await estimateShippingLines(address, API_RETRY); + } else { + const shippingAddressResponse = await callShippingAddressEndpoint(address, false); + if (!shippingAddressResponse.success) { + paymentDataRequestUpdate.error = { + reason: googlePayConstants.GOOGLEPAY_ERROR_REASON_SHIPPING, + message: shippingAddressResponse.error?.message ?? '', + intent + }; + return paymentDataRequestUpdate; + } + shippingLinesResponse = await getShippingLines(API_RETRY); + } + + const {selected_shipping: selectedShipping, available_shipping_lines: shippingLines} = getShipping(); + if (shippingLinesResponse.success) { + if (shippingOptionData && shippingOptionData.id !== BRAINTREE_GOOGLE_EMPTY_SHIPPING_OPTION) { + const option = shippingLines.find(line => line.id === shippingOptionData.id); + option && await changeShippingLine(option.id, API_RETRY); + } else if (!selectedShipping && shippingLines.length > 0) { + await changeShippingLine(shippingLines[0].id, API_RETRY); + } + await getShippingLines(API_RETRY); + } + + if (rsaEnabled) { + await estimateTaxes(address, API_RETRY); + } else { + await setTaxes(API_RETRY); + } + + const {iso_code: currencyCode} = getCurrency(); + const {totalAmountDue} = getTotals(); + paymentDataRequestUpdate.newTransactionInfo = { + currencyCode: currencyCode, + countryCode: countryCode, + totalPrice: getValueByCurrency(totalAmountDue, currencyCode), + totalPriceStatus: 'ESTIMATED' + }; + paymentDataRequestUpdate.newShippingOptionParameters = getPPCPShippingOptionsGoogle(); + return paymentDataRequestUpdate; + } + + default: return paymentDataRequestUpdate; + } +} diff --git a/src/types/paypal.ts b/src/types/paypal.ts index f0f1681..fd994bf 100644 --- a/src/types/paypal.ts +++ b/src/types/paypal.ts @@ -2,11 +2,24 @@ import {PayPalNamespace} from '@paypal/paypal-js'; import ApplePayMerchantCapability = ApplePayJS.ApplePayMerchantCapability; import ApplePayPaymentToken = ApplePayJS.ApplePayPaymentToken; import ApplePayPaymentContact = ApplePayJS.ApplePayPaymentContact; +import GooglePaymentData = google.payments.api.PaymentData; +import GooglePaymentDataRequest = google.payments.api.PaymentDataRequest; +import GoogleTransactionInfo = google.payments.api.TransactionInfo; +import PaymentMethodSpecification = google.payments.api.PaymentMethodSpecification; +import MerchantInfo = google.payments.api.MerchantInfo; +import {IBraintreeGooglePayPaymentData} from 'src/types/braintree'; +import PaymentData = google.payments.api.PaymentData; +import PaymentMethodData = google.payments.api.PaymentMethodData; +import {Address} from '@paypal/paypal-js/types/apis/commons'; export interface IPaypalNamespaceApple extends PayPalNamespace { Applepay: () => IPPCPApplePayInstance; } +export interface IPaypalNamespaceGoogle extends PayPalNamespace { + Googlepay: () => IPPCPGooglePayInstance; +} + export interface IPPCPApplePayInstanceValidateMerchantParam { validationUrl: string; displayName: string; @@ -19,6 +32,14 @@ export interface IPPCPApplePayInstanceConfirmOrderParam { shippingContact?: ApplePayPaymentContact; } +export interface IPPCPGooglePayInstanceConfirmOrderParam { + orderId: string; + paymentMethodData: PaymentMethodData; + billingAddress?: Address; + shippingAddress?: Address; + email?: string, +} + export interface IPPCPApplePayInstance { config: () => Promise; validateMerchant: (validateMerchantParam: IPPCPApplePayInstanceValidateMerchantParam) => Promise; @@ -35,3 +56,21 @@ export interface IPPCPAppleConfig { merchantCapabilities: Array; supportedNetworks: Array; } +export interface IPPCPGooglePayPaymentData { + nonce: string; + type: 'AndroidPayCard' | 'PayPalAccount'; + paymentData: Record; +} + +export interface IPPCPGooglePayInstance { + config: () => Promise; + confirmOrder: (confirmOrderParam: IPPCPGooglePayInstanceConfirmOrderParam) => Promise; +} + +export interface IPPCPGoogleConfig { + apiVersion: number; + apiVersionMinor: number; + countryCode: string, + merchantInfo: MerchantInfo; + allowedPaymentMethods: Array; +} diff --git a/src/types/variables.ts b/src/types/variables.ts index b5d65df..71fe3d0 100644 --- a/src/types/variables.ts +++ b/src/types/variables.ts @@ -6,13 +6,20 @@ import { import {PayPalNamespace} from '@paypal/paypal-js'; import {AmountWithBreakdown, ShippingInfoOption} from '@paypal/paypal-js/types/apis/orders'; import {IBraintreeApplePayInstance, IBraintreeClient, IBraintreeGooglePayInstance} from 'src/types/braintree'; -import {IPaypalNamespaceApple, IPPCPAppleConfig, IPPCPApplePayInstance} from 'src/types/paypal'; +import { + IPaypalNamespaceApple, IPaypalNamespaceGoogle, + IPPCPAppleConfig, + IPPCPApplePayInstance, + IPPCPGoogleConfig, + IPPCPGooglePayInstance +} from 'src/types/paypal'; import GooglePaymentsClient = google.payments.api.PaymentsClient; import ApplePayErrorCode = ApplePayJS.ApplePayErrorCode; import ErrorReason = google.payments.api.ErrorReason; import TransactionState = google.payments.api.TransactionState; import CallbackIntent = google.payments.api.CallbackIntent; import CallbackTrigger = google.payments.api.CallbackTrigger; +import PaymentsClient = google.payments.api.PaymentsClient; export interface IShowPaymentMethods { stripe: boolean; @@ -29,6 +36,7 @@ export interface IShowPaymentMethodTypes { BRAINTREE_GOOGLE: string; BRAINTREE_APPLE: string; PPCP_APPLE: string; + PPCP_GOOGLE: string; PPCP: string } @@ -47,13 +55,17 @@ export interface IActionTypes { } export interface IPaypalState { - paypal: PayPalNamespace | IPaypalNamespaceApple | null; + paypal: PayPalNamespace | IPaypalNamespaceApple | IPaypalNamespaceGoogle | null; paypalPromise: Promise | null; gatewayPublicId: string; ppcpAppleCredentials: IExpressPayPaypalCommercePlatform | null; ppcpApplePayInstance: IPPCPApplePayInstance | null; ppcpApplePayConfig: IPPCPAppleConfig | null; ppcpApplePaySession: ApplePaySession | null; + ppcpGoogleCredentials: IExpressPayPaypalCommercePlatform | null; + ppcpGooglePayInstance: IPPCPGooglePayInstance | null; + ppcpGooglePayConfig: IPPCPGoogleConfig | null; + ppcpGooglePaySession: PaymentsClient | null; } export interface IPaypalConstants { @@ -61,6 +73,7 @@ export interface IPaypalConstants { MAX_STRING_LENGTH: number; APPLEPAY_VERSION_NUMBER: number; APPLEPAY_JS: string; + GOOGLEPAY_JS: string; } export interface IPaypalPatchOperation { @@ -104,6 +117,17 @@ export interface IBraintreeConstants { GOOGLEPAY_VERSION_NUMBER_MINOR: number; } +export interface IGooglePayConstants { + GOOGLEPAY_ERROR_REASON_SHIPPING: ErrorReason; + GOOGLEPAY_ERROR_REASON_PAYMENT: ErrorReason; + GOOGLEPAY_TRANSACTION_STATE_ERROR: TransactionState; + GOOGLEPAY_TRANSACTION_STATE_SUCCESS: TransactionState; + GOOGLEPAY_INTENT_SHIPPING_ADDRESS: CallbackIntent; + GOOGLEPAY_INTENT_SHIPPING_OPTION: CallbackIntent; + GOOGLEPAY_INTENT_PAYMENT_AUTHORIZATION: CallbackIntent; + GOOGLEPAY_TRIGGER_INITIALIZE: CallbackTrigger; +} + export interface IApplePayConstants { APPLEPAY_ERROR_CODE_SHIPPING_CONTACT: ApplePayErrorCode; APPLEPAY_ERROR_CODE_BILLING_CONTACT: ApplePayErrorCode; diff --git a/src/variables/variables.ts b/src/variables/variables.ts index 0df60f9..669e61e 100644 --- a/src/variables/variables.ts +++ b/src/variables/variables.ts @@ -1,4 +1,4 @@ -import { IFastlaneInstance } from 'src/types'; +import {IFastlaneInstance, IGooglePayConstants} from 'src/types'; import { IActionTypes, IApplePayConstants, @@ -26,6 +26,7 @@ export const showPaymentMethodTypes: IShowPaymentMethodTypes = { BRAINTREE_GOOGLE: 'braintreeGoogle', BRAINTREE_APPLE: 'braintreeApple', PPCP_APPLE: 'ppcpApple', + PPCP_GOOGLE: 'ppcpGoogle', PPCP: 'paypalCommercePlatform', }; @@ -49,6 +50,10 @@ export const paypalState: IPaypalState = { ppcpApplePayInstance: null, ppcpApplePayConfig: null, ppcpApplePaySession: null, + ppcpGoogleCredentials: null, + ppcpGooglePayInstance: null, + ppcpGooglePayConfig: null, + ppcpGooglePaySession: null, }; export const paypalConstants: IPaypalConstants = { @@ -56,6 +61,7 @@ export const paypalConstants: IPaypalConstants = { MAX_STRING_LENGTH: 127, APPLEPAY_VERSION_NUMBER: 3, APPLEPAY_JS: 'https://applepay.cdn-apple.com/jsapi/v1/apple-pay-sdk.js', + GOOGLEPAY_JS: 'https://pay.google.com/gp/p/js/pay.js', }; export const braintreeState: IBraintreeState = { @@ -103,6 +109,17 @@ export const applePayConstants: IApplePayConstants = { APPLEPAY_ERROR_CODE_UNKNOWN: 'unknown', }; +export const googlePayConstants: IGooglePayConstants = { + GOOGLEPAY_ERROR_REASON_SHIPPING: 'SHIPPING_ADDRESS_INVALID', + GOOGLEPAY_TRANSACTION_STATE_ERROR: 'ERROR', + GOOGLEPAY_TRANSACTION_STATE_SUCCESS: 'SUCCESS', + GOOGLEPAY_ERROR_REASON_PAYMENT: 'PAYMENT_DATA_INVALID', + GOOGLEPAY_INTENT_SHIPPING_ADDRESS: 'SHIPPING_ADDRESS', + GOOGLEPAY_INTENT_SHIPPING_OPTION: 'SHIPPING_OPTION', + GOOGLEPAY_INTENT_PAYMENT_AUTHORIZATION: 'PAYMENT_AUTHORIZATION', + GOOGLEPAY_TRIGGER_INITIALIZE: 'INITIALIZE', +}; + export const ppcpPayLaterCountryCurrency: Record = { 'AU': 'AUD',