@getopenpay/openpay-js-react 0.0.1-alpha.09717a2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.development +1 -0
- package/.env.production +1 -0
- package/.env.staging +1 -0
- package/.eslintrc.cjs +18 -0
- package/.github/workflows/alpha-release.yml +39 -0
- package/.github/workflows/release.yml +30 -0
- package/Makefile +37 -0
- package/README.md +14 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +3685 -0
- package/example/.eslintrc.json +3 -0
- package/example/Makefile +10 -0
- package/example/README.md +36 -0
- package/example/next.config.mjs +4 -0
- package/example/package-lock.json +5283 -0
- package/example/package.json +27 -0
- package/example/postcss.config.mjs +8 -0
- package/example/public/next.svg +1 -0
- package/example/public/vercel.svg +1 -0
- package/example/src/app/elements/_components/FormWrapper.tsx +18 -0
- package/example/src/app/elements/_components/InputField.tsx +11 -0
- package/example/src/app/elements/card/page.tsx +54 -0
- package/example/src/app/elements/card-cvc/page.tsx +29 -0
- package/example/src/app/elements/card-expiry/page.tsx +29 -0
- package/example/src/app/elements/card-number/page.tsx +29 -0
- package/example/src/app/favicon.ico +0 -0
- package/example/src/app/globals.css +14 -0
- package/example/src/app/layout.tsx +22 -0
- package/example/src/app/page.tsx +119 -0
- package/example/tailwind.config.ts +11 -0
- package/example/tsconfig.json +26 -0
- package/lib/components/_common/frame.tsx +73 -0
- package/lib/components/card-cvc.tsx +11 -0
- package/lib/components/card-expiry.tsx +11 -0
- package/lib/components/card-number.tsx +11 -0
- package/lib/components/card.tsx +11 -0
- package/lib/components/form.tsx +100 -0
- package/lib/hooks/context.ts +17 -0
- package/lib/hooks/use-openpay-elements.ts +12 -0
- package/lib/index.ts +16 -0
- package/lib/utils/constants.ts +1 -0
- package/lib/utils/element.ts +5 -0
- package/lib/utils/event.ts +66 -0
- package/lib/utils/style.ts +17 -0
- package/lib/vite-env.d.ts +1 -0
- package/ngrok.example.yml +6 -0
- package/package.json +44 -0
- package/playwright.config.ts +37 -0
- package/tests/card-cvc.test.ts +68 -0
- package/tests/card-element.test.ts +113 -0
- package/tests/card-expiry.test.ts +61 -0
- package/tests/card-number.test.ts +61 -0
- package/tests/constants.ts +97 -0
- package/tsconfig.build.json +28 -0
- package/tsconfig.json +11 -0
- package/tsconfig.node.json +13 -0
- package/vite.config.ts +36 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "example",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev -p 3032",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"lint": "next lint"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@getopenpay/openpay-js-react": "file:..",
|
|
13
|
+
"next": "14.2.5",
|
|
14
|
+
"react": "^18",
|
|
15
|
+
"react-dom": "^18"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^20",
|
|
19
|
+
"@types/react": "^18",
|
|
20
|
+
"@types/react-dom": "^18",
|
|
21
|
+
"eslint": "^8",
|
|
22
|
+
"eslint-config-next": "14.2.5",
|
|
23
|
+
"postcss": "^8",
|
|
24
|
+
"tailwindcss": "^3.4.1",
|
|
25
|
+
"typescript": "^5"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { FC, PropsWithChildren } from "react";
|
|
2
|
+
|
|
3
|
+
interface FormWrapperProps extends PropsWithChildren {
|
|
4
|
+
error?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const FormWrapper: FC<FormWrapperProps> = (props) => {
|
|
8
|
+
const { children, error } = props;
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="p-8 rounded-lg flex flex-col items-center justify-center bg-emerald-200 dark:bg-emerald-900">
|
|
12
|
+
<div className="max-w-lg w-full">{children}</div>
|
|
13
|
+
<p data-testid="error-message" className="text-red-500">
|
|
14
|
+
{error}
|
|
15
|
+
</p>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FC, PropsWithChildren } from "react";
|
|
2
|
+
|
|
3
|
+
export const InputField: FC<PropsWithChildren> = (props) => {
|
|
4
|
+
const { children } = props;
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
<div className="bg-emerald-50 dark:bg-emerald-800 shadow-md px-4 py-2 rounded-md my-2">
|
|
8
|
+
{children}
|
|
9
|
+
</div>
|
|
10
|
+
);
|
|
11
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { FormWrapper } from "@/app/elements/_components/FormWrapper";
|
|
3
|
+
import { InputField } from "@/app/elements/_components/InputField";
|
|
4
|
+
import { CardElement, ElementsForm } from "@getopenpay/openpay-js-react";
|
|
5
|
+
import { FC, useState } from "react";
|
|
6
|
+
|
|
7
|
+
const ElementsExample: FC = () => {
|
|
8
|
+
const [errors, setErrors] = useState<Record<string, string | undefined>>();
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
|
12
|
+
<div className="w-full max-w-5xl font-mono flex flex-col gap-4">
|
|
13
|
+
<h2 className="box-border text-xl font-bold">
|
|
14
|
+
All card elements in one frame
|
|
15
|
+
</h2>
|
|
16
|
+
<FormWrapper
|
|
17
|
+
error={Object.values(errors ?? {})
|
|
18
|
+
.filter(Boolean)
|
|
19
|
+
.join(". ")}
|
|
20
|
+
>
|
|
21
|
+
<InputField>
|
|
22
|
+
<input
|
|
23
|
+
type="text"
|
|
24
|
+
placeholder="Name"
|
|
25
|
+
className="w-full bg-transparent outline-none text-lg rounded-lg"
|
|
26
|
+
/>
|
|
27
|
+
</InputField>
|
|
28
|
+
|
|
29
|
+
<InputField>
|
|
30
|
+
<ElementsForm
|
|
31
|
+
onValidationError={(message, elementId) => {
|
|
32
|
+
if (elementId) {
|
|
33
|
+
setErrors((errors) => ({ ...errors, [elementId]: message }));
|
|
34
|
+
}
|
|
35
|
+
}}
|
|
36
|
+
onChange={(elemId) => {
|
|
37
|
+
if (elemId) {
|
|
38
|
+
setErrors((errors) => ({
|
|
39
|
+
...errors,
|
|
40
|
+
[elemId]: "",
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<CardElement />
|
|
46
|
+
</ElementsForm>
|
|
47
|
+
</InputField>
|
|
48
|
+
</FormWrapper>
|
|
49
|
+
</div>
|
|
50
|
+
</main>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default ElementsExample;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { FormWrapper } from "@/app/elements/_components/FormWrapper";
|
|
3
|
+
import { InputField } from "@/app/elements/_components/InputField";
|
|
4
|
+
import { CardCvcElement, ElementsForm } from "@getopenpay/openpay-js-react";
|
|
5
|
+
import { FC, useState } from "react";
|
|
6
|
+
|
|
7
|
+
const ElementsExample: FC = () => {
|
|
8
|
+
const [error, setError] = useState<string>("");
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
|
12
|
+
<div className="w-full max-w-5xl font-mono flex flex-col gap-4">
|
|
13
|
+
<h2 className="text-xl font-bold">Card CVC element only</h2>
|
|
14
|
+
<FormWrapper error={error}>
|
|
15
|
+
<InputField>
|
|
16
|
+
<ElementsForm
|
|
17
|
+
onValidationError={(message) => setError(message)}
|
|
18
|
+
onChange={() => setError("")}
|
|
19
|
+
>
|
|
20
|
+
<CardCvcElement />
|
|
21
|
+
</ElementsForm>
|
|
22
|
+
</InputField>
|
|
23
|
+
</FormWrapper>
|
|
24
|
+
</div>
|
|
25
|
+
</main>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default ElementsExample;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { FormWrapper } from "@/app/elements/_components/FormWrapper";
|
|
3
|
+
import { InputField } from "@/app/elements/_components/InputField";
|
|
4
|
+
import { CardExpiryElement, ElementsForm } from "@getopenpay/openpay-js-react";
|
|
5
|
+
import { FC, useState } from "react";
|
|
6
|
+
|
|
7
|
+
const ElementsExample: FC = () => {
|
|
8
|
+
const [error, setError] = useState<string>("");
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
|
12
|
+
<div className="w-full max-w-5xl font-mono flex flex-col gap-4">
|
|
13
|
+
<h2 className="text-xl font-bold">Card expiry element only</h2>
|
|
14
|
+
<FormWrapper error={error}>
|
|
15
|
+
<InputField>
|
|
16
|
+
<ElementsForm
|
|
17
|
+
onValidationError={(message) => setError(message)}
|
|
18
|
+
onChange={() => setError("")}
|
|
19
|
+
>
|
|
20
|
+
<CardExpiryElement />
|
|
21
|
+
</ElementsForm>
|
|
22
|
+
</InputField>
|
|
23
|
+
</FormWrapper>
|
|
24
|
+
</div>
|
|
25
|
+
</main>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default ElementsExample;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { FormWrapper } from "@/app/elements/_components/FormWrapper";
|
|
3
|
+
import { InputField } from "@/app/elements/_components/InputField";
|
|
4
|
+
import { CardNumberElement, ElementsForm } from "@getopenpay/openpay-js-react";
|
|
5
|
+
import { FC, useState } from "react";
|
|
6
|
+
|
|
7
|
+
const ElementsExample: FC = () => {
|
|
8
|
+
const [error, setError] = useState<string>("");
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
|
12
|
+
<div className="w-full max-w-5xl font-mono flex flex-col gap-4">
|
|
13
|
+
<h2 className="text-xl font-bold">Card number element only</h2>
|
|
14
|
+
<FormWrapper error={error}>
|
|
15
|
+
<InputField>
|
|
16
|
+
<ElementsForm
|
|
17
|
+
onValidationError={(message) => setError(message)}
|
|
18
|
+
onChange={() => setError("")}
|
|
19
|
+
>
|
|
20
|
+
<CardNumberElement />
|
|
21
|
+
</ElementsForm>
|
|
22
|
+
</InputField>
|
|
23
|
+
</FormWrapper>
|
|
24
|
+
</div>
|
|
25
|
+
</main>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default ElementsExample;
|
|
Binary file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
body {
|
|
6
|
+
@apply bg-emerald-50 text-gray-900;
|
|
7
|
+
font-family: 'Inter', sans-serif;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@media (prefers-color-scheme: dark) {
|
|
11
|
+
body {
|
|
12
|
+
@apply bg-emerald-950 text-gray-100;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Inter } from "next/font/google";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
|
|
5
|
+
const inter = Inter({ subsets: ["latin"] });
|
|
6
|
+
|
|
7
|
+
export const metadata: Metadata = {
|
|
8
|
+
title: "Create Next App",
|
|
9
|
+
description: "Generated by create next app",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default function RootLayout({
|
|
13
|
+
children,
|
|
14
|
+
}: Readonly<{
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}>) {
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<body className={inter.className}>{children}</body>
|
|
20
|
+
</html>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { FC, PropsWithChildren, useState } from 'react';
|
|
3
|
+
import { ElementsForm, CardCvcElement, CardElement, CardNumberElement, CardExpiryElement } from '@getopenpay/openpay-js-react';
|
|
4
|
+
|
|
5
|
+
interface FormWrapperProps extends PropsWithChildren {
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const FormWrapper: FC<FormWrapperProps> = (props) => {
|
|
10
|
+
const { children, error } = props;
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="p-8 rounded-lg flex flex-col items-center justify-center bg-emerald-200 dark:bg-emerald-900">
|
|
14
|
+
<div className="max-w-lg w-full">
|
|
15
|
+
{children}
|
|
16
|
+
</div>
|
|
17
|
+
<p className='text-red-500'>{error}</p>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const InputField: FC<PropsWithChildren> = (props) => {
|
|
23
|
+
const { children } = props;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="bg-emerald-50 dark:bg-emerald-800 shadow-md px-4 py-2 rounded-md my-2">{children}</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ElementsExample: FC = () => {
|
|
31
|
+
const [error1, setError1] = useState<string>('');
|
|
32
|
+
const [error2, setError2] = useState<string>('');
|
|
33
|
+
const [error3, setError3] = useState<string>('');
|
|
34
|
+
const [error4, setError4] = useState<string>('');
|
|
35
|
+
const [error5, setError5] = useState<string>('');
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
|
39
|
+
<div className="w-full max-w-5xl font-mono flex flex-col gap-4">
|
|
40
|
+
<h1 className="text-2xl font-bold">OpenPay Elements</h1>
|
|
41
|
+
<p className="text-lg">Accept payments through OpenPay, right on your site</p>
|
|
42
|
+
<hr />
|
|
43
|
+
|
|
44
|
+
<h2 className="box-border text-xl font-bold">All card elements in one frame</h2>
|
|
45
|
+
<FormWrapper error={error1}>
|
|
46
|
+
<InputField>
|
|
47
|
+
<input type="text" placeholder="Name" className="w-full bg-transparent outline-none text-lg rounded-lg" />
|
|
48
|
+
</InputField>
|
|
49
|
+
|
|
50
|
+
<InputField>
|
|
51
|
+
<ElementsForm
|
|
52
|
+
onValidationError={(message) => setError1(message)}
|
|
53
|
+
onChange={() => setError1('')}
|
|
54
|
+
>
|
|
55
|
+
<CardElement />
|
|
56
|
+
</ElementsForm>
|
|
57
|
+
</InputField>
|
|
58
|
+
</FormWrapper>
|
|
59
|
+
|
|
60
|
+
<h2 className="box-border text-xl font-bold">All card elements in separate frames</h2>
|
|
61
|
+
<FormWrapper error={error2}>
|
|
62
|
+
<InputField>
|
|
63
|
+
<input type="text" placeholder="Name" className="w-full bg-transparent outline-none text-lg rounded-lg" />
|
|
64
|
+
</InputField>
|
|
65
|
+
|
|
66
|
+
<InputField>
|
|
67
|
+
<ElementsForm
|
|
68
|
+
className="flex gap-2 items-center justify-between"
|
|
69
|
+
onValidationError={(message) => setError2(message)}
|
|
70
|
+
onChange={() => setError2('')}
|
|
71
|
+
>
|
|
72
|
+
<CardNumberElement />
|
|
73
|
+
<CardExpiryElement />
|
|
74
|
+
<CardCvcElement />
|
|
75
|
+
</ElementsForm>
|
|
76
|
+
</InputField>
|
|
77
|
+
</FormWrapper>
|
|
78
|
+
|
|
79
|
+
<h2 className="text-xl font-bold">Card number element only</h2>
|
|
80
|
+
<FormWrapper error={error3}>
|
|
81
|
+
<InputField>
|
|
82
|
+
<ElementsForm
|
|
83
|
+
onValidationError={(message) => setError3(message)}
|
|
84
|
+
onChange={() => setError3('')}
|
|
85
|
+
>
|
|
86
|
+
<CardNumberElement />
|
|
87
|
+
</ElementsForm>
|
|
88
|
+
</InputField>
|
|
89
|
+
</FormWrapper>
|
|
90
|
+
|
|
91
|
+
<h2 className="text-xl font-bold">Card expiry element only</h2>
|
|
92
|
+
<FormWrapper error={error4}>
|
|
93
|
+
<InputField>
|
|
94
|
+
<ElementsForm
|
|
95
|
+
onValidationError={(message) => setError4(message)}
|
|
96
|
+
onChange={() => setError4('')}
|
|
97
|
+
>
|
|
98
|
+
<CardExpiryElement />
|
|
99
|
+
</ElementsForm>
|
|
100
|
+
</InputField>
|
|
101
|
+
</FormWrapper>
|
|
102
|
+
|
|
103
|
+
<h2 className="text-xl font-bold">Card CVC element only</h2>
|
|
104
|
+
<FormWrapper error={error5}>
|
|
105
|
+
<InputField>
|
|
106
|
+
<ElementsForm
|
|
107
|
+
onValidationError={(message) => setError5(message)}
|
|
108
|
+
onChange={() => setError5('')}
|
|
109
|
+
>
|
|
110
|
+
<CardCvcElement />
|
|
111
|
+
</ElementsForm>
|
|
112
|
+
</InputField>
|
|
113
|
+
</FormWrapper>
|
|
114
|
+
</div>
|
|
115
|
+
</main>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export default ElementsExample;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Config } from "tailwindcss";
|
|
2
|
+
|
|
3
|
+
const config: Config = {
|
|
4
|
+
content: [
|
|
5
|
+
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
|
6
|
+
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
7
|
+
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
|
8
|
+
],
|
|
9
|
+
plugins: [],
|
|
10
|
+
};
|
|
11
|
+
export default config;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
4
|
+
"allowJs": true,
|
|
5
|
+
"skipLibCheck": true,
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"module": "esnext",
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"jsx": "preserve",
|
|
14
|
+
"incremental": true,
|
|
15
|
+
"plugins": [
|
|
16
|
+
{
|
|
17
|
+
"name": "next"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"paths": {
|
|
21
|
+
"@/*": ["./src/*"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
25
|
+
"exclude": ["node_modules"]
|
|
26
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { FRAME_BASE_URL } from '../../utils/constants';
|
|
3
|
+
import { convertStylesToQueryString, type ElementStyle } from '../../utils/style';
|
|
4
|
+
import { useOpenPayElements } from '../../hooks/use-openpay-elements';
|
|
5
|
+
|
|
6
|
+
type ElementFrameProps = {
|
|
7
|
+
subPath: string;
|
|
8
|
+
styles?: ElementStyle;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const ElementFrame: FC<ElementFrameProps> = (props) => {
|
|
12
|
+
const { subPath, styles } = props;
|
|
13
|
+
const { contextId, dispatchEvent, formHeight } = useOpenPayElements();
|
|
14
|
+
const [referer, setReferer] = useState<string>('');
|
|
15
|
+
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
16
|
+
|
|
17
|
+
const elementStyle = useMemo(() => {
|
|
18
|
+
if (!styles) return '';
|
|
19
|
+
return convertStylesToQueryString(styles);
|
|
20
|
+
}, [styles]);
|
|
21
|
+
|
|
22
|
+
const frameStyle = useMemo(() => {
|
|
23
|
+
return {
|
|
24
|
+
border: 'none',
|
|
25
|
+
width: '100%',
|
|
26
|
+
height: formHeight,
|
|
27
|
+
overflow: 'hidden',
|
|
28
|
+
};
|
|
29
|
+
}, [formHeight]);
|
|
30
|
+
|
|
31
|
+
const queryString = useMemo(() => {
|
|
32
|
+
const params = new URLSearchParams();
|
|
33
|
+
|
|
34
|
+
params.append('referer', referer);
|
|
35
|
+
params.append('styles', elementStyle);
|
|
36
|
+
params.append('contextId', contextId);
|
|
37
|
+
|
|
38
|
+
return params.toString();
|
|
39
|
+
}, [elementStyle, contextId, referer]);
|
|
40
|
+
|
|
41
|
+
const onMessage = useCallback(
|
|
42
|
+
(event: MessageEvent) => {
|
|
43
|
+
if (!iframeRef.current || event.origin == window.location.origin) return;
|
|
44
|
+
|
|
45
|
+
if (event.origin !== FRAME_BASE_URL) {
|
|
46
|
+
console.warn('[form] Ignoring message from unexpected origin:', event, '(expected:', FRAME_BASE_URL, ')');
|
|
47
|
+
} else {
|
|
48
|
+
dispatchEvent(event, iframeRef.current);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
[dispatchEvent]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!iframeRef.current || !contextId) return;
|
|
56
|
+
window.addEventListener('message', onMessage);
|
|
57
|
+
setReferer(window.location.origin);
|
|
58
|
+
|
|
59
|
+
// Ensure cleanup
|
|
60
|
+
return () => window.removeEventListener('message', onMessage);
|
|
61
|
+
}, [iframeRef, contextId, onMessage]);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<iframe
|
|
65
|
+
name={`${subPath}-element`}
|
|
66
|
+
src={`${FRAME_BASE_URL}/app/v1/${subPath}-element?${queryString}`}
|
|
67
|
+
style={frameStyle}
|
|
68
|
+
ref={iframeRef}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default ElementFrame;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
import { type ElementProps } from '../utils/element';
|
|
3
|
+
import ElementFrame from './_common/frame';
|
|
4
|
+
|
|
5
|
+
const CardCvcElement: FC<ElementProps> = ({ styles }) => {
|
|
6
|
+
return (
|
|
7
|
+
<ElementFrame styles={styles} subPath="card-cvc" />
|
|
8
|
+
);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default CardCvcElement;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
import { type ElementProps } from '../utils/element';
|
|
3
|
+
import ElementFrame from './_common/frame';
|
|
4
|
+
|
|
5
|
+
const CardExpiryElement: FC<ElementProps> = ({ styles }) => {
|
|
6
|
+
return (
|
|
7
|
+
<ElementFrame styles={styles} subPath="card-expiry" />
|
|
8
|
+
);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default CardExpiryElement;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
import { type ElementProps } from '../utils/element';
|
|
3
|
+
import ElementFrame from './_common/frame';
|
|
4
|
+
|
|
5
|
+
const CardNumberElement: FC<ElementProps> = ({ styles }) => {
|
|
6
|
+
return (
|
|
7
|
+
<ElementFrame styles={styles} subPath="card-number" />
|
|
8
|
+
);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default CardNumberElement;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
import { type ElementProps } from '../utils/element';
|
|
3
|
+
import ElementFrame from './_common/frame';
|
|
4
|
+
|
|
5
|
+
const CardElement: FC<ElementProps> = ({ styles }) => {
|
|
6
|
+
return (
|
|
7
|
+
<ElementFrame styles={styles} subPath="card" />
|
|
8
|
+
);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default CardElement;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { FC, PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { ElementsContext, type ElementsContextValue } from '../hooks/context';
|
|
3
|
+
import { emitEvent, ElementEventType, parseEventPayload } from '../utils/event';
|
|
4
|
+
|
|
5
|
+
interface ElementsFormProps extends PropsWithChildren {
|
|
6
|
+
className?: string;
|
|
7
|
+
onFocus?: (elementId: string) => void;
|
|
8
|
+
onBlur?: (elementId: string) => void;
|
|
9
|
+
onChange?: (elementId: string) => void;
|
|
10
|
+
onValidationError?: (message: string, elementId?: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const ElementsForm: FC<ElementsFormProps> = (props) => {
|
|
14
|
+
const { children, className, onFocus, onBlur, onChange, onValidationError } = props;
|
|
15
|
+
const [contextId, setContextId] = useState<string>('');
|
|
16
|
+
const [nonces, setNonces] = useState<string[]>([]);
|
|
17
|
+
const [targets, setTargets] = useState<Record<string, HTMLIFrameElement>>({});
|
|
18
|
+
const [formHeight, setFormHeight] = useState<string>('1px');
|
|
19
|
+
const formRef = useRef<HTMLDivElement | null>(null);
|
|
20
|
+
|
|
21
|
+
useEffect(() => setContextId(`op-elements-${window.crypto.randomUUID()}`), []);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!formRef.current) return;
|
|
25
|
+
|
|
26
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
27
|
+
entries.forEach((entry) => {
|
|
28
|
+
if (!entry.target) return;
|
|
29
|
+
const { width } = entry.contentRect;
|
|
30
|
+
|
|
31
|
+
// Resize all target iframes
|
|
32
|
+
Object.values(targets).forEach((target) => {
|
|
33
|
+
if (!target) return;
|
|
34
|
+
emitEvent(target, contextId, 'root', ElementEventType.RESIZE, { width: width.toString() });
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
resizeObserver.observe(formRef.current);
|
|
40
|
+
|
|
41
|
+
return () => {
|
|
42
|
+
resizeObserver.disconnect();
|
|
43
|
+
};
|
|
44
|
+
}, [formRef, contextId, targets]);
|
|
45
|
+
|
|
46
|
+
const dispatchEvent = useCallback(
|
|
47
|
+
(event: MessageEvent, frame: HTMLIFrameElement) => {
|
|
48
|
+
const eventData = parseEventPayload(JSON.parse(event.data));
|
|
49
|
+
|
|
50
|
+
if (eventData.formId !== contextId) {
|
|
51
|
+
console.warn('[form] Ignoring event for different form. Expected:', contextId, 'Got:', eventData.formId);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (eventData.nonce in nonces) {
|
|
56
|
+
console.warn('[form] Ignoring duplicate event:', eventData);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setNonces((prevNonces) => [...prevNonces, eventData.nonce]);
|
|
61
|
+
console.log('[form] Received event:', eventData);
|
|
62
|
+
|
|
63
|
+
if (eventData.type === ElementEventType.VALIDATION_ERROR && !!onValidationError) {
|
|
64
|
+
onValidationError(eventData.payload['message'], eventData.elementId);
|
|
65
|
+
} else if (eventData.type === ElementEventType.FOCUS && !!onFocus) {
|
|
66
|
+
onFocus(eventData.elementId);
|
|
67
|
+
} else if (eventData.type === ElementEventType.BLUR && !!onBlur) {
|
|
68
|
+
onBlur(eventData.elementId);
|
|
69
|
+
} else if (eventData.type === ElementEventType.CHANGE && !!onChange) {
|
|
70
|
+
onChange(eventData.elementId);
|
|
71
|
+
} else if (eventData.type === ElementEventType.LOADED) {
|
|
72
|
+
const elementId = eventData.elementId;
|
|
73
|
+
if (!elementId) return;
|
|
74
|
+
|
|
75
|
+
setTargets((prevTargets) => ({ ...prevTargets, [elementId]: frame }));
|
|
76
|
+
|
|
77
|
+
const height = eventData.payload.height ? `${eventData.payload.height}px` : '100%';
|
|
78
|
+
setFormHeight(height);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
[contextId, nonces, onValidationError, onFocus, onBlur, onChange]
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const value: ElementsContextValue = {
|
|
85
|
+
contextId,
|
|
86
|
+
createToken: () => {},
|
|
87
|
+
dispatchEvent,
|
|
88
|
+
formHeight,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<ElementsContext.Provider value={value}>
|
|
93
|
+
<div className={className} ref={formRef}>
|
|
94
|
+
{children}
|
|
95
|
+
</div>
|
|
96
|
+
</ElementsContext.Provider>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default ElementsForm;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
|
|
3
|
+
export type ElementsContextValue = {
|
|
4
|
+
contextId: string;
|
|
5
|
+
createToken: () => void;
|
|
6
|
+
dispatchEvent: (event: MessageEvent, frame: HTMLIFrameElement) => void;
|
|
7
|
+
formHeight: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const ElementsContext = createContext<ElementsContextValue>({
|
|
11
|
+
contextId: '',
|
|
12
|
+
createToken: () => {},
|
|
13
|
+
dispatchEvent: () => {},
|
|
14
|
+
formHeight: '',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export default { ElementsContext };
|