From 52a4fdb7880163cac1cb5aaad5f6545b41e2c1e7 Mon Sep 17 00:00:00 2001 From: Simon Paris Date: Thu, 26 Feb 2026 14:29:41 +0700 Subject: [PATCH] applepay Add docs --- README.md | 12 +++ package.json | 1 + pnpm-lock.yaml | 8 ++ sdk/build.ts | 1 + sdk/src/backend-types/digital-wallets.ts | 4 + .../channel-picker-digital-wallet-section.tsx | 8 ++ .../components/digital-wallet-applepay.tsx | 89 +++++++++++++++++++ .../components/digital-wallet-container.tsx | 22 ++++- sdk/src/data/test-data.ts | 13 +++ sdk/src/public-data-types.ts | 2 +- sdk/src/public-options-types.ts | 4 +- sdk/src/styles.css | 15 ++++ tsconfig.common.json | 2 +- 13 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 sdk/src/components/digital-wallet-applepay.tsx diff --git a/README.md b/README.md index 218a83a..03fa7d2 100644 --- a/README.md +++ b/README.md @@ -409,6 +409,18 @@ You can customize the appearance of the button using the options parameter of `c To use Google Pay, you must adhere to the Google Pay and Wallet API's [Acceptable Use Policy](https://payments.developers.google.com/terms/aup) and accept the terms defined in the [Google Pay API Terms of Service](https://payments.developers.google.com/terms/sellertos). Additionally, please ensure you follow the [Google Pay brand guidelines](https://developers.google.com/pay/api/web/guides/brand-guidelines). --> + + ## Troubleshooting ### Usage with React Strict Mode diff --git a/package.json b/package.json index 183a003..15a2664 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", + "@types/applepayjs": "^14.0.9", "@types/googlepay": "^0.7.10", "@types/mime-types": "^3.0.1", "@types/node": "^25.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9aadd87..2821419 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,9 @@ importers: '@testing-library/user-event': specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.1) + '@types/applepayjs': + specifier: ^14.0.9 + version: 14.0.9 '@types/googlepay': specifier: ^0.7.10 version: 0.7.10 @@ -897,6 +900,9 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@types/applepayjs@14.0.9': + resolution: {integrity: sha512-xEprYbb0TEP/XIiDPbVnTYpDai8fTFpsQfVSfTd81Is2GOMUy7ie019eyX6Mz2ECxfjoUVKaiGSL577roIeHCg==} + '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} @@ -3005,6 +3011,8 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 + '@types/applepayjs@14.0.9': {} + '@types/argparse@1.0.38': {} '@types/aria-query@5.0.4': {} diff --git a/sdk/build.ts b/sdk/build.ts index 33e5c3e..09d105b 100755 --- a/sdk/build.ts +++ b/sdk/build.ts @@ -243,6 +243,7 @@ async function generateTestPage() { + `; diff --git a/sdk/src/backend-types/digital-wallets.ts b/sdk/src/backend-types/digital-wallets.ts index 5c97a95..e28d284 100644 --- a/sdk/src/backend-types/digital-wallets.ts +++ b/sdk/src/backend-types/digital-wallets.ts @@ -7,4 +7,8 @@ export type BffDigitalWallets = { payment_method_specification: google.payments.api.PaymentMethodSpecification; }[]; }; + apple_pay?: { + merchant_id: "mock-applepay-merchant-id"; + apple_pay_payment_request: ApplePayJS.ApplePayPaymentRequest; + }; }; diff --git a/sdk/src/components/channel-picker-digital-wallet-section.tsx b/sdk/src/components/channel-picker-digital-wallet-section.tsx index c7cef05..b51f377 100644 --- a/sdk/src/components/channel-picker-digital-wallet-section.tsx +++ b/sdk/src/components/channel-picker-digital-wallet-section.tsx @@ -34,6 +34,14 @@ export const ChannelPickerDigitalWalletSection: FunctionComponent = (props) => { } }, []); + useLayoutEffect(() => { + if (containerRef.current) { + containerRef.current.appendChild( + sdk.createDigitalWalletComponent("APPLE_PAY"), + ); + } + }, [sdk]); + return (
; + onReady: () => void; +}; + +export const DigitalWalletApplepay: FunctionComponent = (props) => { + const { onReady } = props; + + const session = useSession(); + + const digitalWallets = useDigitalWallets(); + assert(digitalWallets); + const digitalWalletsApplePay = digitalWallets.apple_pay; + assert(digitalWalletsApplePay); + + const didCallReady = useRef(false); + + const onClick = () => { + const applePayData = digitalWalletsApplePay; + assert(applePayData); + + const applePaySession = new ApplePaySession( + 3, + digitalWalletsApplePay.apple_pay_payment_request, + ); + + applePaySession.onvalidatemerchant = async (event) => { + try { + // const validationURL = event.validationURL; + // TODO: call merchant validation endpoint to get merchant session + applePaySession.completeMerchantValidation({}); + } catch (err) { + console.error("Error validating Apple Pay merchant:", err); + applePaySession.abort(); + } + }; + + applePaySession.onpaymentauthorized = async (event) => { + // TODO: submit payment + }; + + applePaySession.begin(); + }; + + useLayoutEffect(() => { + if (didCallReady.current) { + return; + } + if ( + ApplePaySession.supportsVersion(3) && + ApplePaySession.canMakePayments() + ) { + onReady(); + didCallReady.current = true; + } + }, [onReady]); + + return ( + + ); +}; + +declare module "react/jsx-runtime" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "apple-pay-button": preact.DetailedHTMLProps< + { + onClick: () => void; + className?: string; + type?: "plain" | "buy" | "donate" | "checkout"; + locale?: string; + }, + HTMLDivElement + >; + } + } +} diff --git a/sdk/src/components/digital-wallet-container.tsx b/sdk/src/components/digital-wallet-container.tsx index fab6364..0f95d15 100644 --- a/sdk/src/components/digital-wallet-container.tsx +++ b/sdk/src/components/digital-wallet-container.tsx @@ -2,8 +2,9 @@ import { FunctionComponent, JSX } from "preact"; import { DigitalWalletGooglepay } from "./digital-wallet-googlepay"; import { DigitalWalletOptions } from "../public-options-types"; import { XenditDigitalWalletCode } from "../public-data-types"; -import { useCallback, useRef } from "preact/hooks"; import { DigitalWalletWaitForLoad } from "./digital-wallet-wait-for-load"; +import { DigitalWalletApplepay } from "./digital-wallet-applepay"; +import { useCallback, useRef } from "preact/hooks"; type Props = { digitalWalletCode: T; @@ -41,6 +42,20 @@ export const DigitalWalletContainer: FunctionComponent< ); break; } + case "APPLE_PAY": { + el = ( + + + + ); + break; + } } return
{el}
; @@ -52,6 +67,11 @@ const sdkStatusCheckers = { checkLoaded: () => typeof google !== "undefined" && typeof google.payments !== "undefined", }, + APPLE_PAY: { + scriptTagRegex: + /https:\/\/applepay.cdn-apple.com\/jsapi\/.*\/apple-pay-sdk.js/, + checkLoaded: () => typeof ApplePaySession !== "undefined", + }, }; export class InternalDigitalWalletReady extends Event { diff --git a/sdk/src/data/test-data.ts b/sdk/src/data/test-data.ts index 394a9d1..1a75779 100644 --- a/sdk/src/data/test-data.ts +++ b/sdk/src/data/test-data.ts @@ -1343,6 +1343,19 @@ export function makeTestBffData(): BffResponse { }, ], }, + apple_pay: { + merchant_id: "mock-applepay-merchant-id", + apple_pay_payment_request: { + merchantCapabilities: ["supports3DS"], + supportedNetworks: ["VISA", "MASTERCARD", "AMEX"], + countryCode: "ID", + currencyCode: "IDR", + total: { + label: "Total", + amount: "10000", + }, + } satisfies ApplePayJS.ApplePayPaymentRequest, + }, }, }; } diff --git a/sdk/src/public-data-types.ts b/sdk/src/public-data-types.ts index 3a79a53..2160893 100644 --- a/sdk/src/public-data-types.ts +++ b/sdk/src/public-data-types.ts @@ -265,7 +265,7 @@ export interface XenditPaymentChannelGroup { /** * @public */ -export type XenditDigitalWalletCode = "GOOGLE_PAY"; +export type XenditDigitalWalletCode = "GOOGLE_PAY" | "APPLE_PAY"; /** * @public diff --git a/sdk/src/public-options-types.ts b/sdk/src/public-options-types.ts index 0591634..19a147d 100644 --- a/sdk/src/public-options-types.ts +++ b/sdk/src/public-options-types.ts @@ -116,4 +116,6 @@ export type DigitalWalletOptions = buttonSizeMode?: "fill" | "static"; buttonBorderType?: "no_border" | "default_border"; } - : never; + : T extends "APPLE_PAY" + ? object + : never; diff --git a/sdk/src/styles.css b/sdk/src/styles.css index 7104a3f..e7f2fd4 100644 --- a/sdk/src/styles.css +++ b/sdk/src/styles.css @@ -1189,3 +1189,18 @@ xendit-payment-channel[inert] { .gpay-card-info-container-fill { display: flex; } + +/* applepay */ + +.xendit-apple-pay-button { + --apple-pay-button-height: 42px; + --apple-pay-button-border-radius: 999px; + display: block; + cursor: pointer; + width: 100%; + box-sizing: border-box; + border: none; + outline: none; + margin: -1px 0; + overflow: hidden; +} diff --git a/tsconfig.common.json b/tsconfig.common.json index 8407123..84ff42e 100644 --- a/tsconfig.common.json +++ b/tsconfig.common.json @@ -13,7 +13,7 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - "types": ["googlepay"], + "types": ["googlepay", "applepayjs"], "paths": { "react/jsx-runtime": ["./node_modules/preact/jsx-runtime"] }