Skip to content
Draft
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
-->

<!--
### Apple Pay™

Apple Pay is available to Sessions that meet the following requirements:

- Your Session country is a country where Apple Pay is supported.
- You have cards available as a payment method and your available cards include at least one card network supported by Apple Pay.
- You've provided your Apple MerchantID to Xendit on the Xendit Dashboard.
- The [Apple Pay SDK](https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js) is loaded. (You need to include the SDK yourself, we don't bundle it. There's no need to wait for it to finish loading, if it's still loading when you create the component, it'll have `display:none` until it finishes loading)
- The user is using a compatible browser. (The component will have `display:none` if the user is using an incompatible browser)
-->

## Troubleshooting

### Usage with React Strict Mode
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions sdk/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ async function generateTestPage() {
</head>
<body>
<script async src="https://pay.google.com/gp/p/js/pay.js"></script>
<script async src="https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js"></script>
<script type="module">${stripTypeScriptTypes(code)}</script>
</body>
</html>`;
Expand Down
4 changes: 4 additions & 0 deletions sdk/src/backend-types/digital-wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ export const ChannelPickerDigitalWalletSection: FunctionComponent = (props) => {
}
}, []);

useLayoutEffect(() => {
if (containerRef.current) {
containerRef.current.appendChild(
sdk.createDigitalWalletComponent("APPLE_PAY"),
);
}
}, [sdk]);

return (
<div
ref={containerRef}
Expand Down
89 changes: 89 additions & 0 deletions sdk/src/components/digital-wallet-applepay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { FunctionComponent } from "preact";
import { DigitalWalletOptions } from "../public-options-types";
import { useDigitalWallets, useSession } from "./session-provider";
import { assert } from "../utils";
import { useLayoutEffect, useRef } from "preact/hooks";

type Props = {
options?: DigitalWalletOptions<"APPLE_PAY">;
onReady: () => void;
};

export const DigitalWalletApplepay: FunctionComponent<Props> = (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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would need to bump the version to 14 to support recurring payments.

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 (
<apple-pay-button
onClick={onClick}
className="xendit-apple-pay-button"
type="plain"
locale={session.locale}
/>
);
};

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
>;
}
}
}
22 changes: 21 additions & 1 deletion sdk/src/components/digital-wallet-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends XenditDigitalWalletCode> = {
digitalWalletCode: T;
Expand Down Expand Up @@ -41,6 +42,20 @@ export const DigitalWalletContainer: FunctionComponent<
);
break;
}
case "APPLE_PAY": {
el = (
<DigitalWalletWaitForLoad
scriptTagRegex={sdkStatusCheckers.APPLE_PAY.scriptTagRegex}
checkLoaded={sdkStatusCheckers.APPLE_PAY.checkLoaded}
>
<DigitalWalletApplepay
onReady={onReady}
options={digitalWalletOptions}
/>
</DigitalWalletWaitForLoad>
);
break;
}
}

return <div ref={containerRef}>{el}</div>;
Expand All @@ -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 {
Expand Down
13 changes: 13 additions & 0 deletions sdk/src/data/test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
};
}
2 changes: 1 addition & 1 deletion sdk/src/public-data-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ export interface XenditPaymentChannelGroup {
/**
* @public
*/
export type XenditDigitalWalletCode = "GOOGLE_PAY";
export type XenditDigitalWalletCode = "GOOGLE_PAY" | "APPLE_PAY";

/**
* @public
Expand Down
4 changes: 3 additions & 1 deletion sdk/src/public-options-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,6 @@ export type DigitalWalletOptions<T extends XenditDigitalWalletCode> =
buttonSizeMode?: "fill" | "static";
buttonBorderType?: "no_border" | "default_border";
}
: never;
: T extends "APPLE_PAY"
? object
: never;
15 changes: 15 additions & 0 deletions sdk/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion tsconfig.common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
Expand Down
Loading