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 7 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;
}
}
}
212 changes: 212 additions & 0 deletions apps/web/components/PayPal/GooglePayButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
<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 { PayPalAddToCartCallback } from '~/components/PayPal/types';
import { cartGetters, orderGetters } from '@plentymarkets/shop-api';

let isGooglePayLoaded = true;
let countryCodeString = 'DE';
pfrincu-plenty marked this conversation as resolved.
Show resolved Hide resolved
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: any): Promise<google.payments.api.PaymentAuthorizationResult> {
pfrincu-plenty marked this conversation as resolved.
Show resolved Hide resolved
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: any) {
pfrincu-plenty marked this conversation as resolved.
Show resolved Hide resolved
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,
token: paymentData.token,
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: any) => {
pfrincu-plenty marked this conversation as resolved.
Show resolved Hide resolved
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: any) {
pfrincu-plenty marked this conversation as resolved.
Show resolved Hide resolved
return {
transactionState: 'ERROR',
error: {
message: error.message,
},
};
}
}
onMounted(async () => {
await loadGooglePay().then(() => {
if (google && (paypal as any).Googlepay) {
onGooglePayLoaded().catch(console.error);

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

View workflow job for this annotation

GitHub Actions / Lint

Avoid nesting promises
}
return null;
});
});
</script>
6 changes: 6 additions & 0 deletions apps/web/components/PayPal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,9 @@ export type ApplepayType = {
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 @@ -45,6 +45,7 @@
- Added label tags for inputs in `NewsletterSubscribe.vue` component.
- Optimize aria labels and alt texts on homepage
- Added cache-control for all images in order to solve "serve static assets" problem.
- 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 @@ -4447,6 +4447,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 @@ -5295,6 +5296,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