Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement googlepay payment method #653

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/web/assets/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,12 @@ apple-pay-button {
--apple-pay-button-border-radius: 7px;
--apple-pay-button-box-sizing: border-box;
}

#google-pay-button {
div {
button {
width: 100% !important;
display: block;
}
}
}
213 changes: 213 additions & 0 deletions apps/web/components/PayPal/GooglePayButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
<template>
<div class="flex items-center justify-center w-full my-2" v-if="isGooglePayLoaded">
<div class="border-t-2 flex-grow"></div>
<p class="px-2 text-sm uppercase text-gray-400">{{ $t('or') }}</p>
<div class="border-t-2 flex-grow"></div>
</div>
<div id="google-pay-button"></div>
</template>

<script lang="ts" setup>
import { GooglePayPayerActionData, PayPalAddToCartCallback } from '~/components/PayPal/types';
import { cartGetters, orderGetters } from '@plentymarkets/shop-api';

let isGooglePayLoaded = true;
let countryCodeString = '';
const { loadScript, executeOrder, createTransaction, captureOrder } = usePayPal();
const { shippingPrivacyAgreement } = useAdditionalInformation();
const { createOrder } = useMakeOrder();
const { data: cart, clearCartItems } = useCart();
const currency = computed(() => cartGetters.getCurrency(cart.value) || (useAppConfig().fallbackCurrency as string));
const paypal = await loadScript(currency.value);
const localePath = useLocalePath();
const emits = defineEmits<{
(event: 'button-clicked', callback: PayPalAddToCartCallback): Promise<void>;
}>();
const loadGooglePay = async () => {
return new Promise((resolve, reject) => {
const scriptElement = document.createElement('script');
scriptElement.src = 'https://pay.google.com/gp/p/js/pay.js';
scriptElement.type = 'text/javascript';
scriptElement.addEventListener('load', resolve);
// eslint-disable-next-line unicorn/prefer-add-event-listener
scriptElement.onerror = reject;
document.head.append(scriptElement);
});
};

let paymentsClient: google.payments.api.PaymentsClient | null = null,
googlepayConfig: any = null;

async function getGooglePayConfig() {
if (googlepayConfig === null) {
googlepayConfig = await (paypal as any).Googlepay().config();
}
return googlepayConfig;
}

async function getGooglePaymentDataRequest() {
const {
allowedPaymentMethods,
merchantInfo,
apiVersion,
apiVersionMinor,
countryCode,
transactionInfo,
callbackIntents,
} = await getGooglePayConfig();
countryCodeString = countryCode;
const baseRequest = {
apiVersion,
apiVersionMinor,
allowedPaymentMethods,
transactionInfo,
merchantInfo,
callbackIntents,
};
const paymentDataRequest = Object.assign({}, baseRequest);
paymentDataRequest.allowedPaymentMethods = allowedPaymentMethods;
paymentDataRequest.transactionInfo = getGoogleTransactionInfo();
paymentDataRequest.merchantInfo = merchantInfo;
paymentDataRequest.callbackIntents = ['PAYMENT_AUTHORIZATION'];
return paymentDataRequest;
}

function onPaymentAuthorized(
paymentData: google.payments.api.PaymentData,
): Promise<google.payments.api.PaymentAuthorizationResult> {
return new Promise<google.payments.api.PaymentAuthorizationResult>((resolve) => {
processPayment(paymentData)
// eslint-disable-next-line promise/always-return
.then(() => {
resolve({
transactionState: 'SUCCESS',
} as google.payments.api.PaymentAuthorizationResult);
})
.catch((error) => {
resolve({
transactionState: 'ERROR',
error: {
message: error.message,
},
} as google.payments.api.PaymentAuthorizationResult);
});
});
}

function getGooglePaymentsClient() {
if (paymentsClient === null) {
paymentsClient = new google.payments.api.PaymentsClient({
environment: 'TEST',
paymentDataCallbacks: {
onPaymentAuthorized: onPaymentAuthorized,
},
});
}
return paymentsClient;
}

async function onGooglePayLoaded() {
const paymentsClient = getGooglePaymentsClient();
const { allowedPaymentMethods, apiVersion, apiVersionMinor } = await getGooglePayConfig();
try {
const response = await paymentsClient.isReadyToPay({ allowedPaymentMethods, apiVersion, apiVersionMinor });
isGooglePayLoaded = response.result;
if (response.result) {
addGooglePayButton();
}
} catch (error) {
console.error(error);
}
}

function addGooglePayButton() {
const paymentsClient = getGooglePaymentsClient();
const button = paymentsClient.createButton({
onClick: onGooglePaymentButtonClicked,
});
const theContainer = document.querySelector('#google-pay-button');
if (theContainer) {
theContainer.append(button);
}
}

function getGoogleTransactionInfo() {
return {
countryCode: countryCodeString,
currencyCode: currency.value,
totalPriceStatus: 'FINAL',
totalPrice: cartGetters.getTotals(cart.value).total.toString(),
};
}

async function onGooglePaymentButtonClicked() {
emits('button-clicked', async (successfully) => {
if (successfully) {
const paymentDataRequest = await getGooglePaymentDataRequest();
const paymentsClient = getGooglePaymentsClient();
paymentsClient.loadPaymentData(paymentDataRequest);
}
});
}

async function processPayment(paymentData: google.payments.api.PaymentData) {
try {
const transaction = await createTransaction('paypal');
if (!transaction || !transaction.id) throw new Error('Transaction creation failed.');
const order = await createOrder({
paymentId: cart.value.methodOfPaymentId,
shippingPrivacyHintAccepted: shippingPrivacyAgreement.value,
});
if (!order || !order.order || !order.order.id) throw new Error('Order creation failed.');

const { status } = await (paypal as any).Googlepay().confirmOrder({
orderId: transaction.id,
paymentMethodData: paymentData.paymentMethodData,
});

if (status === 'PAYER_ACTION_REQUIRED') {
// eslint-disable-next-line promise/catch-or-return
(paypal as any)
.Googlepay()
.initiatePayerAction({ orderId: order.order.id })
// eslint-disable-next-line promise/always-return
.then(async (data: GooglePayPayerActionData) => {
await captureOrder({
paypalOrderId: data.paypalOrderId,
paypalPayerId: data.paypalPayerId,
});
await executeOrder({
mode: 'paypal',
plentyOrderId: Number.parseInt(orderGetters.getId(order)),
paypalTransactionId: data.orderID,
});
});
} else {
await executeOrder({
mode: 'paypal',
plentyOrderId: Number.parseInt(orderGetters.getId(order)),
paypalTransactionId: transaction.id,
});
}
clearCartItems();
navigateTo(localePath(paths.confirmation + '/' + order.order.id + '/' + order.order.accessKey));

return { transactionState: 'SUCCESS' };
} catch (error: unknown) {
return {
transactionState: 'ERROR',
error: {
message: error,
},
};
}
}
onMounted(async () => {
await loadGooglePay().then(() => {
if (google && (paypal as any).Googlepay) {
onGooglePayLoaded().catch(console.error);

Check warning on line 208 in apps/web/components/PayPal/GooglePayButton.vue

View workflow job for this annotation

GitHub Actions / Lint

Avoid nesting promises
}
return null;
});
});
</script>
12 changes: 12 additions & 0 deletions apps/web/components/PayPal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,20 @@ export type ValidateMerchantResponse = {
paypalDebugId: null | string;
};

export type GooglePayPayerActionData = {
paypalOrderId: string;
paypalPayerId: string;
orderID: string;
};

export type ApplepayType = {
config(): Promise<ConfigResponse>;
validateMerchant(argument0: ValidateMerchantParams): Promise<ValidateMerchantResponse>;
confirmOrder(argument0: ConfirmOrderParams): Promise<void>;
};

export type GooglepayType = {
config(): Promise<ConfigResponse>;
validateMerchant(argument0: ValidateMerchantParams): Promise<ValidateMerchantResponse>;
confirmOrder(argument0: ConfirmOrderParams): Promise<void>;
};
2 changes: 1 addition & 1 deletion apps/web/composables/usePayPal/usePayPal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export const usePayPal: UsePayPalMethodsReturn = () => {
currency: currency,
dataPartnerAttributionId: 'Plenty_Cart_PWA_PPCP',
components:
'applepay,messages,buttons,funding-eligibility,card-fields,payment-fields,marks&enable-funding=paylater',
'googlepay,applepay,messages,buttons,funding-eligibility,card-fields,payment-fields,marks&enable-funding=paylater',
locale: localePayPal,
commit: commit,
});
Expand Down
4 changes: 4 additions & 0 deletions apps/web/pages/checkout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@
:style="createOrderLoading || disableShippingPayment || cartLoading ? 'pointer-events: none;' : ''"
@button-clicked="validateTerms"
/>
<PayPalGooglePayButton
:style="createOrderLoading || disableShippingPayment || cartLoading ? 'pointer-events: none;' : ''"
@button-clicked="validateTerms"
/>
</OrderSummary>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"types": ["vitest/globals", "@vite-pwa/nuxt", "@types/applepayjs"],
"types": ["vitest/globals", "@vite-pwa/nuxt", "@types/applepayjs", "@types/googlepay"],
"verbatimModuleSyntax": false,
},
"exclude": ["node_modules", "mocks", "__tests__", "cypress.config.ts"],
Expand Down
1 change: 1 addition & 0 deletions docs/changelog/changelog_de.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
- Das Logo kann jetzt vom plentysystems-System geladen werden.
- Die "Erneut kaufen"-Funktionalität unterstützt Artikeleigenschaften.
- PayPal-Button für PS Lazyload
- Googlepay payment method

### Geändert

Expand Down
1 change: 1 addition & 0 deletions docs/changelog/changelog_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
- Added cache-control for all images in order to solve "serve static assets" problem.
- Added table header in the MyAccount.
- Updated Nuxt to 3.13.1 (includes vue 3.5.0) for increased performance and stability.
- Implement Googlepay payment method

### 🩹 Fixed

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"dependencies": {
"@plentymarkets/shop-api": "^0.58.0",
"@types/applepayjs": "^14.0.8",
"@types/googlepay": "^0.7.6",
"@vee-validate/nuxt": "^4.13.1",
"@vee-validate/yup": "^4.13.1",
"country-flag-icons": "^1.5.12",
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4238,6 +4238,7 @@ __metadata:
"@paypal/paypal-js": 8.1.0
"@plentymarkets/shop-api": ^0.58.0
"@types/applepayjs": ^14.0.8
"@types/googlepay": ^0.7.6
"@types/uuid": ^9.0.8
"@vee-validate/nuxt": ^4.13.1
"@vee-validate/yup": ^4.13.1
Expand Down Expand Up @@ -4974,6 +4975,13 @@ __metadata:
languageName: node
linkType: hard

"@types/googlepay@npm:^0.7.6":
version: 0.7.6
resolution: "@types/googlepay@npm:0.7.6"
checksum: 6874d432fbf0badb9af24911d6c8fa446ed91e63267b83a281da3a79f591fd3bbe85656c03859adfd6de21f8d0355acdc515ba810515569ccdd40d1c4c40cdab
languageName: node
linkType: hard

"@types/hash-sum@npm:^1.0.2":
version: 1.0.2
resolution: "@types/hash-sum@npm:1.0.2"
Expand Down
Loading