@hello-pay/react 0.3.0 → 0.5.1
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/README.md +11 -3
- package/dist/index.cjs +100 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +200 -7
- package/dist/index.d.ts +200 -7
- package/dist/index.js +99 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
CardExpiryElement,
|
|
22
22
|
CardCvcElement,
|
|
23
23
|
CardholderNameElement,
|
|
24
|
+
CardPasswordElement,
|
|
24
25
|
useHelloPay,
|
|
25
26
|
useElements,
|
|
26
27
|
} from '@hello-pay/react';
|
|
@@ -46,17 +47,22 @@ function PaymentForm() {
|
|
|
46
47
|
if (!hellopay || !elements) return;
|
|
47
48
|
|
|
48
49
|
const cardNumber = elements.getElement('cardNumber');
|
|
49
|
-
const { token, card, error } = await hellopay.createToken(cardNumber);
|
|
50
|
+
const { token, provider, card, error } = await hellopay.createToken(cardNumber);
|
|
50
51
|
|
|
51
52
|
if (error) {
|
|
52
53
|
// error.type は 'validation_error' | 'api_error' | 'config_error'
|
|
53
54
|
showError(error.message);
|
|
54
55
|
return;
|
|
55
56
|
}
|
|
56
|
-
// 以降は token / card が必ず non-null(Discriminated Union)
|
|
57
|
+
// 以降は token / provider / card が必ず non-null(Discriminated Union)
|
|
58
|
+
// provider は通常 'hellopay'(HelloPay の決済 API に渡す)。
|
|
59
|
+
// Mastercard 暫定対応を利用している場合のみ、プロジェクトの設定に応じて
|
|
60
|
+
// 'stripe' / 'gmo' になる。いずれもトークンは加盟店自身の PSP 契約上に
|
|
61
|
+
// 発行されるため、決済・3DS・返金は自社の実装で行う。
|
|
62
|
+
// 'gmo' のトークンは発行から 30 分・1 回限りで、期限は tokenExpiryDate に入る。
|
|
57
63
|
await fetch('/api/charge', {
|
|
58
64
|
method: 'POST',
|
|
59
|
-
body: JSON.stringify({ token, last4: card.last4 }),
|
|
65
|
+
body: JSON.stringify({ token, provider, last4: card.last4 }),
|
|
60
66
|
});
|
|
61
67
|
}
|
|
62
68
|
|
|
@@ -66,6 +72,8 @@ function PaymentForm() {
|
|
|
66
72
|
<CardExpiryElement />
|
|
67
73
|
<CardCvcElement />
|
|
68
74
|
<CardholderNameElement />
|
|
75
|
+
{/* 韓国発行カードのみ: 暗証番号の先頭 2 桁 */}
|
|
76
|
+
<CardPasswordElement />
|
|
69
77
|
<button type="submit">支払う</button>
|
|
70
78
|
</form>
|
|
71
79
|
);
|
package/dist/index.cjs
CHANGED
|
@@ -20,9 +20,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
ApplePayButton: () => ApplePayButton,
|
|
23
24
|
CardCvcElement: () => CardCvcElement,
|
|
24
25
|
CardExpiryElement: () => CardExpiryElement,
|
|
25
26
|
CardNumberElement: () => CardNumberElement,
|
|
27
|
+
CardPasswordElement: () => CardPasswordElement,
|
|
26
28
|
CardholderNameElement: () => CardholderNameElement,
|
|
27
29
|
Elements: () => Elements,
|
|
28
30
|
loadHelloPay: () => loadHelloPay,
|
|
@@ -181,6 +183,24 @@ function useElements() {
|
|
|
181
183
|
|
|
182
184
|
// src/ElementComponents.tsx
|
|
183
185
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
186
|
+
var PROBE_PX = 1e5;
|
|
187
|
+
function contentSizedAxes(container) {
|
|
188
|
+
if (container.getClientRects().length === 0) {
|
|
189
|
+
const computed = getComputedStyle(container);
|
|
190
|
+
const isAuto = (v) => v === "" || v === "auto";
|
|
191
|
+
return { width: isAuto(computed.width), height: isAuto(computed.height) };
|
|
192
|
+
}
|
|
193
|
+
const before = { width: container.offsetWidth, height: container.offsetHeight };
|
|
194
|
+
const probe = container.ownerDocument.createElement("div");
|
|
195
|
+
probe.style.cssText = `width: ${PROBE_PX}px; height: ${PROBE_PX}px;`;
|
|
196
|
+
container.appendChild(probe);
|
|
197
|
+
const grows = {
|
|
198
|
+
width: container.offsetWidth > before.width,
|
|
199
|
+
height: container.offsetHeight > before.height
|
|
200
|
+
};
|
|
201
|
+
probe.remove();
|
|
202
|
+
return grows;
|
|
203
|
+
}
|
|
184
204
|
function createElementComponent(type) {
|
|
185
205
|
function ElementComponent({
|
|
186
206
|
options,
|
|
@@ -193,12 +213,19 @@ function createElementComponent(type) {
|
|
|
193
213
|
}) {
|
|
194
214
|
const elements = useElements();
|
|
195
215
|
const containerRef = (0, import_react3.useRef)(null);
|
|
216
|
+
const [fill, setFill] = (0, import_react3.useState)({ width: false, height: false });
|
|
196
217
|
const handlersRef = (0, import_react3.useRef)({ onReady, onChange, onFocus, onBlur });
|
|
197
218
|
handlersRef.current = { onReady, onChange, onFocus, onBlur };
|
|
198
219
|
(0, import_react3.useEffect)(() => {
|
|
199
|
-
|
|
220
|
+
const container = containerRef.current;
|
|
221
|
+
if (!elements || !container) return;
|
|
222
|
+
const unset = contentSizedAxes(container);
|
|
223
|
+
setFill((prev) => ({
|
|
224
|
+
width: prev.width || unset.width,
|
|
225
|
+
height: prev.height || unset.height
|
|
226
|
+
}));
|
|
200
227
|
const element = elements.create(type, options);
|
|
201
|
-
element.mount(
|
|
228
|
+
element.mount(container);
|
|
202
229
|
const unsubs = [
|
|
203
230
|
element.on("ready", (e) => handlersRef.current.onReady?.(e)),
|
|
204
231
|
element.on("change", (e) => handlersRef.current.onChange?.(e)),
|
|
@@ -215,7 +242,11 @@ function createElementComponent(type) {
|
|
|
215
242
|
{
|
|
216
243
|
ref: containerRef,
|
|
217
244
|
className,
|
|
218
|
-
style: {
|
|
245
|
+
style: {
|
|
246
|
+
...fill.width && { width: "100%" },
|
|
247
|
+
...fill.height && { height: "100%" },
|
|
248
|
+
...style
|
|
249
|
+
}
|
|
219
250
|
}
|
|
220
251
|
);
|
|
221
252
|
}
|
|
@@ -227,11 +258,77 @@ var CardNumberElement = createElementComponent("cardNumber");
|
|
|
227
258
|
var CardExpiryElement = createElementComponent("cardExpiry");
|
|
228
259
|
var CardCvcElement = createElementComponent("cardCvc");
|
|
229
260
|
var CardholderNameElement = createElementComponent("cardholderName");
|
|
261
|
+
var CardPasswordElement = createElementComponent("cardPassword");
|
|
262
|
+
|
|
263
|
+
// src/ApplePayButton.tsx
|
|
264
|
+
var import_react4 = require("react");
|
|
265
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
266
|
+
function ApplePayButton({
|
|
267
|
+
options,
|
|
268
|
+
onToken,
|
|
269
|
+
onError,
|
|
270
|
+
onCancel,
|
|
271
|
+
buttonStyle = "black",
|
|
272
|
+
buttonType = "plain",
|
|
273
|
+
className,
|
|
274
|
+
style
|
|
275
|
+
}) {
|
|
276
|
+
const hellopay = useHelloPay();
|
|
277
|
+
const buttonRef = (0, import_react4.useRef)(null);
|
|
278
|
+
const [supported, setSupported] = (0, import_react4.useState)(false);
|
|
279
|
+
(0, import_react4.useEffect)(() => {
|
|
280
|
+
setSupported(!!hellopay && hellopay.canMakeApplePayPayments());
|
|
281
|
+
}, [hellopay]);
|
|
282
|
+
(0, import_react4.useEffect)(() => {
|
|
283
|
+
const el = buttonRef.current;
|
|
284
|
+
if (!el) return;
|
|
285
|
+
el.style.setProperty("-webkit-appearance", "-apple-pay-button");
|
|
286
|
+
el.style.setProperty("-apple-pay-button-type", buttonType);
|
|
287
|
+
el.style.setProperty("-apple-pay-button-style", buttonStyle);
|
|
288
|
+
}, [supported, buttonType, buttonStyle]);
|
|
289
|
+
const latest = (0, import_react4.useRef)({ options, onToken, onError, onCancel, hellopay });
|
|
290
|
+
latest.current = { options, onToken, onError, onCancel, hellopay };
|
|
291
|
+
if (!supported) return null;
|
|
292
|
+
const handleClick = () => {
|
|
293
|
+
const current = latest.current;
|
|
294
|
+
if (!current.hellopay) return;
|
|
295
|
+
current.hellopay.applePay(current.options).then((result) => {
|
|
296
|
+
if (result.error) {
|
|
297
|
+
if (result.error.type === "cancel") {
|
|
298
|
+
current.onCancel?.();
|
|
299
|
+
} else {
|
|
300
|
+
current.onError?.(result.error);
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
current.onToken(result.token, result.card);
|
|
305
|
+
}).catch((err) => {
|
|
306
|
+
current.onError?.({
|
|
307
|
+
type: "config_error",
|
|
308
|
+
code: "apple_pay_error",
|
|
309
|
+
message: err instanceof Error ? err.message : "Apple Pay error"
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
};
|
|
313
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
314
|
+
"button",
|
|
315
|
+
{
|
|
316
|
+
ref: buttonRef,
|
|
317
|
+
type: "button",
|
|
318
|
+
onClick: handleClick,
|
|
319
|
+
className,
|
|
320
|
+
style: { width: "100%", height: "44px", cursor: "pointer", ...style }
|
|
321
|
+
}
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
ApplePayButton.displayName = "HelloPay(ApplePayButton)";
|
|
230
325
|
// Annotate the CommonJS export names for ESM import in node:
|
|
231
326
|
0 && (module.exports = {
|
|
327
|
+
ApplePayButton,
|
|
232
328
|
CardCvcElement,
|
|
233
329
|
CardExpiryElement,
|
|
234
330
|
CardNumberElement,
|
|
331
|
+
CardPasswordElement,
|
|
235
332
|
CardholderNameElement,
|
|
236
333
|
Elements,
|
|
237
334
|
loadHelloPay,
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/loadHelloPay.ts","../src/Elements.tsx","../src/ElementComponents.tsx","../src/hooks.ts"],"sourcesContent":["export { loadHelloPay } from './loadHelloPay';\nexport type { LoadHelloPayOptions } from './loadHelloPay';\n\nexport { Elements } from './Elements';\nexport type { ElementsProps } from './Elements';\n\nexport {\n CardNumberElement,\n CardExpiryElement,\n CardCvcElement,\n CardholderNameElement,\n} from './ElementComponents';\nexport type { ElementProps } from './ElementComponents';\n\nexport { useHelloPay, useElements } from './hooks';\n\nexport type {\n HelloPay,\n Elements as ElementsSession,\n Element,\n ElementType,\n ElementOptions,\n ElementStyle,\n ElementChangeEvent,\n ElementFocusEvent,\n ElementReadyEvent,\n TokenCard,\n HelloPayError,\n CreateTokenResult,\n Unsubscribe,\n} from './types';\n","/**\n * HelloPay SDK ローダー。\n *\n * HelloPay 管理下のドメイン(js.hello-pay.app 等)から SDK を動的ロードし、\n * `window.HelloPay` ファクトリを解決する。\n *\n * - 同一 sdkUrl への呼び出しは script tag を 1 回だけ挿入(dedup)\n * - 異なる sdkUrl は独立したロードを維持(E2E / ローカル切り替え対応)\n * - SSR 環境では Promise<null> を解決(加盟店側で null チェック)\n * - 一時的な失敗はキャッシュせず次回呼び出しで再試行可能\n */\n\nimport type { HelloPay, HelloPayFactory } from './types';\n\n// SDK 本体は test / live どちらの publishable key でも同一ドメイン\n// (js.hello-pay.app) から配信する。test / live mode の切り替えは SDK 内部で\n// publishable key の prefix から解決し、test mode では接続先を test 環境\n// (*.stg.hello-pay.app) に向ける(hellopay.js と同じ方式)。\nconst DEFAULT_SDK_URL = 'https://js.hello-pay.app/v1/hellopay-elements.js';\n\n// sdkUrl 単位で script ロード Promise をキャッシュ。\n// loadPromise を単一変数にするとすべての URL で共有されてしまい、\n// 異なる sdkUrl(E2E / ローカル開発)が誤って同一ロードを共有する。\nconst scriptPromiseCache = new Map<string, Promise<HelloPayFactory | null>>();\n\n// publicKey + locale + sdkUrl の組み合わせごとに `HelloPaySDK` インスタンスをキャッシュ。\n// 加盟店が誤って inline で loadHelloPay を呼び出した場合でも同一 SDK インスタンスが\n// 返るようにして、Elements セッションの BroadcastChannel session ID ミスマッチを防ぐ。\nconst sdkCache = new Map<string, Promise<HelloPay | null>>();\n\n/** テスト専用: module-scope のキャッシュをすべて初期状態に戻す。 */\nexport function __resetLoadPromiseForTest(): void {\n scriptPromiseCache.clear();\n sdkCache.clear();\n}\n\nfunction normalizeSdkUrl(url: string): string {\n if (typeof window === 'undefined') return url;\n\n try {\n return new URL(url, window.location.href).href;\n } catch {\n return url;\n }\n}\n\nfunction normalizeLocale(locale?: string): string {\n return locale || 'ja';\n}\n\nfunction loadScript(url: string): Promise<HelloPayFactory | null> {\n if (typeof window === 'undefined') return Promise.resolve(null);\n\n // URL を正規化してプロトコル省略形などを統一する。\n // テスト / SSR 環境では window.location が http URL でない場合があるため try-catch で保護。\n const normalizedUrl = normalizeSdkUrl(url);\n\n return new Promise((resolve, reject) => {\n // script は URL 単位で管理する。異なる URL の script を削除せず共存させることで、\n // E2E / ローカル開発で異なる sdkUrl が並列で呼ばれても Promise が宙吊りにならない。\n // 状態は dataset.hellopayState に \"loading\" | \"loaded\" | \"error\" で刻む。\n const managedScripts = Array.from(\n document.querySelectorAll<HTMLScriptElement>('script[data-hellopay-sdk]'),\n );\n\n const existing = managedScripts.find((script) => {\n const rawUrl = script.dataset.hellopayUrl || script.src;\n if (!rawUrl) return false;\n try {\n return new URL(rawUrl, window.location.href).href === normalizedUrl;\n } catch {\n return rawUrl === normalizedUrl;\n }\n });\n\n if (existing) {\n const state = existing.dataset.hellopayState;\n if (state === 'loaded') {\n resolve(window.HelloPay ?? null);\n return;\n }\n if (state === 'loading') {\n existing.addEventListener('load', () => resolve(window.HelloPay ?? null), {\n once: true,\n });\n existing.addEventListener(\n 'error',\n () => reject(new Error('HelloPay SDK failed to load')),\n { once: true },\n );\n return;\n }\n // state === \"error\" or undefined: 失敗済み or 未知の状態なので作り直す\n existing.remove();\n }\n\n const script = document.createElement('script');\n script.src = normalizedUrl;\n script.async = true;\n script.dataset.hellopaySdk = 'true';\n script.dataset.hellopayUrl = normalizedUrl; // URL 比較用\n script.dataset.hellopayState = 'loading';\n script.addEventListener(\n 'load',\n () => {\n if (!window.HelloPay) {\n script.dataset.hellopayState = 'error';\n reject(new Error('HelloPay SDK loaded but window.HelloPay is missing'));\n return;\n }\n script.dataset.hellopayState = 'loaded';\n resolve(window.HelloPay);\n },\n { once: true },\n );\n script.addEventListener(\n 'error',\n () => {\n script.dataset.hellopayState = 'error';\n reject(new Error('HelloPay SDK failed to load'));\n },\n { once: true },\n );\n document.head.appendChild(script);\n });\n}\n\nexport interface LoadHelloPayOptions {\n /** SDK 配信 URL(ローカル開発・E2E で差し替える用。省略時は js.hello-pay.app から配信し、test/live は SDK 内部で key prefix から解決) */\n sdkUrl?: string;\n locale?: string;\n}\n\n/**\n * HelloPay SDK をロードして初期化する。\n * モジュールスコープで 1 回だけ呼び、返り値の Promise を `<Elements>` に渡す。\n */\nexport function loadHelloPay(\n publicKey: string,\n options: LoadHelloPayOptions = {},\n): Promise<HelloPay | null> {\n const sdkUrl = normalizeSdkUrl(options.sdkUrl ?? DEFAULT_SDK_URL);\n const locale = normalizeLocale(options.locale);\n const cacheKey = `${publicKey}::${locale}::${sdkUrl}`;\n const cached = sdkCache.get(cacheKey);\n if (cached) return cached;\n\n let scriptPromise = scriptPromiseCache.get(sdkUrl);\n if (!scriptPromise) {\n scriptPromise = loadScript(sdkUrl).catch((err) => {\n // 一時的なロード失敗はキャッシュせず次回呼び出しで再試行可能にする\n scriptPromiseCache.delete(sdkUrl);\n throw err;\n });\n scriptPromiseCache.set(sdkUrl, scriptPromise);\n }\n\n const sdkPromise = scriptPromise\n .then((factory) => {\n if (!factory) return null; // SSR\n return factory(publicKey, { locale: options.locale });\n })\n .catch((err) => {\n // SDK インスタンス生成失敗は sdkCache から退避して次回呼び出しで再試行可能にする\n sdkCache.delete(cacheKey);\n throw err;\n });\n sdkCache.set(cacheKey, sdkPromise);\n return sdkPromise;\n}\n","/**\n * <Elements> Provider.\n *\n * loadHelloPay() の Promise を受け取り、SDK インスタンスと Elements セッションを\n * 子孫に Context 経由で供給する。\n */\n\nimport { createContext, useEffect, useState, type ReactNode } from 'react';\nimport type { HelloPay, Elements as ElementsSession } from './types';\n\nexport interface ElementsContextValue {\n hellopay: HelloPay | null;\n elements: ElementsSession | null;\n}\n\nconst EMPTY_CONTEXT: ElementsContextValue = {\n hellopay: null,\n elements: null,\n};\n\nexport const ElementsContext = createContext<ElementsContextValue>(EMPTY_CONTEXT);\n\nexport interface ElementsProps {\n /** loadHelloPay() の戻り値。モジュールスコープで 1 回だけ生成すること */\n hellopay: Promise<HelloPay | null> | HelloPay | null;\n children: ReactNode;\n}\n\nexport function Elements({ hellopay: hellopayProp, children }: ElementsProps) {\n const [ctx, setCtx] = useState<ElementsContextValue>(EMPTY_CONTEXT);\n\n useEffect(() => {\n let cancelled = false;\n setCtx(EMPTY_CONTEXT);\n\n Promise.resolve(hellopayProp)\n .then((sdk) => {\n if (cancelled) return;\n if (!sdk) {\n setCtx(EMPTY_CONTEXT);\n return;\n }\n setCtx({ hellopay: sdk, elements: sdk.elements() });\n })\n .catch(() => {\n // SDK ロード失敗時は null のまま(unhandled rejection を防ぐ)\n if (cancelled) return;\n setCtx(EMPTY_CONTEXT);\n });\n\n return () => {\n cancelled = true;\n };\n }, [hellopayProp]);\n\n return <ElementsContext.Provider value={ctx}>{children}</ElementsContext.Provider>;\n}\n","/**\n * Element コンポーネント。\n *\n * 各コンポーネントは 1 つの `<div>` コンテナを描画するだけで、\n * 実体(カード情報が入る iframe)はコア SDK が差し込む。\n *\n * React Strict Mode の二重 mount 対応:\n * - effect の cleanup で element.unmount() を必ず呼ぶ\n * - unmount 後は Elements._elements 配列から除外される(コア SDK 側で対応済)\n * - 結果として Strict Mode で effect が再実行されても古い iframe は残らない\n */\n\nimport { useEffect, useRef, type CSSProperties } from 'react';\nimport { useElements } from './hooks';\nimport type {\n ElementType,\n ElementOptions,\n ElementChangeEvent,\n ElementFocusEvent,\n ElementReadyEvent,\n} from './types';\n\nexport interface ElementProps {\n options?: ElementOptions;\n className?: string;\n style?: CSSProperties;\n onReady?: (e: ElementReadyEvent) => void;\n onChange?: (e: ElementChangeEvent) => void;\n onFocus?: (e: ElementFocusEvent) => void;\n onBlur?: (e: ElementFocusEvent) => void;\n}\n\n/**\n * 指定 type の Element を管理する React コンポーネントを生成する。\n */\nfunction createElementComponent(type: ElementType) {\n function ElementComponent({\n options,\n className,\n style,\n onReady,\n onChange,\n onFocus,\n onBlur,\n }: ElementProps) {\n const elements = useElements();\n const containerRef = useRef<HTMLDivElement | null>(null);\n\n // 最新のハンドラを ref で保持(effect を毎回 re-run させないため)\n const handlersRef = useRef({ onReady, onChange, onFocus, onBlur });\n handlersRef.current = { onReady, onChange, onFocus, onBlur };\n\n useEffect(() => {\n if (!elements || !containerRef.current) return;\n\n const element = elements.create(type, options);\n element.mount(containerRef.current);\n\n const unsubs = [\n element.on('ready', (e) => handlersRef.current.onReady?.(e)),\n element.on('change', (e) => handlersRef.current.onChange?.(e)),\n element.on('focus', (e) => handlersRef.current.onFocus?.(e)),\n element.on('blur', (e) => handlersRef.current.onBlur?.(e)),\n ];\n\n return () => {\n unsubs.forEach((u) => u());\n element.unmount();\n };\n // options はマウント時にしか反映しない方針(Stripe と同じ挙動)。\n // 動的に style を変えたい場合は後続 PR で element.update() を追加。\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [elements]);\n\n // 親コンテナ(加盟店が指定した `.element-container` 等)の高さを\n // iframe に伝播させるため、wrapper は親のサイズを塗りつぶす。\n // style prop で上書き可能。\n return (\n <div\n ref={containerRef}\n className={className}\n style={{ width: '100%', height: '100%', ...style }}\n />\n );\n }\n ElementComponent.displayName = `HelloPay(${type})`;\n // getElement(CardNumberElement) のようにコンポーネント自身を型指定子として使えるよう、\n // 静的プロパティに type を生やしておく\n ElementComponent.__elementType = type as ElementType;\n return ElementComponent;\n}\n\nexport const CardNumberElement = createElementComponent('cardNumber');\nexport const CardExpiryElement = createElementComponent('cardExpiry');\nexport const CardCvcElement = createElementComponent('cardCvc');\nexport const CardholderNameElement = createElementComponent('cardholderName');\n\nexport type ElementComponent = typeof CardNumberElement;\n","/**\n * React hooks.\n *\n * useHelloPay / useElements は <Elements> Provider の Context から\n * SDK インスタンスと Elements セッションを取り出す。\n * SDK ロード完了までは null を返すので、ハンドラ内で null チェックする。\n */\n\nimport { useContext } from 'react';\nimport { ElementsContext } from './Elements';\nimport type { HelloPay, Elements } from './types';\n\nexport function useHelloPay(): HelloPay | null {\n return useContext(ElementsContext).hellopay;\n}\n\nexport function useElements(): Elements | null {\n return useContext(ElementsContext).elements;\n}\n"],"mappings":"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","names":["script","import_react","import_react","import_jsx_runtime"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/loadHelloPay.ts","../src/Elements.tsx","../src/ElementComponents.tsx","../src/hooks.ts","../src/ApplePayButton.tsx"],"sourcesContent":["export { loadHelloPay } from './loadHelloPay';\nexport type { LoadHelloPayOptions } from './loadHelloPay';\n\nexport { Elements } from './Elements';\nexport type { ElementsProps } from './Elements';\n\nexport {\n CardNumberElement,\n CardExpiryElement,\n CardCvcElement,\n CardholderNameElement,\n CardPasswordElement,\n} from './ElementComponents';\nexport type { ElementProps } from './ElementComponents';\n\nexport { ApplePayButton } from './ApplePayButton';\nexport type { ApplePayButtonProps } from './ApplePayButton';\n\nexport { useHelloPay, useElements } from './hooks';\n\nexport type {\n HelloPay,\n Elements as ElementsSession,\n Element,\n ElementType,\n ElementOptions,\n ElementStyle,\n ElementChangeEvent,\n CardNumberChangeEvent,\n OtherElementChangeEvent,\n IssuerCountryResult,\n ElementFocusEvent,\n ElementReadyEvent,\n TokenCard,\n HelloPayError,\n CreateTokenResult,\n TokenProvider,\n ApplePayCard,\n ApplePayOptions,\n ApplePayResult,\n ApplePayError,\n ApplePayCancelError,\n Unsubscribe,\n} from './types';\n","/**\n * HelloPay SDK ローダー。\n *\n * HelloPay 管理下のドメイン(js.hello-pay.app 等)から SDK を動的ロードし、\n * `window.HelloPay` ファクトリを解決する。\n *\n * - 同一 sdkUrl への呼び出しは script tag を 1 回だけ挿入(dedup)\n * - 異なる sdkUrl は独立したロードを維持(E2E / ローカル切り替え対応)\n * - SSR 環境では Promise<null> を解決(加盟店側で null チェック)\n * - 一時的な失敗はキャッシュせず次回呼び出しで再試行可能\n */\n\nimport type { HelloPay, HelloPayFactory } from './types';\n\n// SDK 本体は test / live どちらの publishable key でも同一ドメイン\n// (js.hello-pay.app) から配信する。test / live mode の切り替えは SDK 内部で\n// publishable key の prefix から解決し、test mode では接続先を test 環境\n// (*.stg.hello-pay.app) に向ける(hellopay.js と同じ方式)。\nconst DEFAULT_SDK_URL = 'https://js.hello-pay.app/v1/hellopay-elements.js';\n\n// sdkUrl 単位で script ロード Promise をキャッシュ。\n// loadPromise を単一変数にするとすべての URL で共有されてしまい、\n// 異なる sdkUrl(E2E / ローカル開発)が誤って同一ロードを共有する。\nconst scriptPromiseCache = new Map<string, Promise<HelloPayFactory | null>>();\n\n// publicKey + locale + sdkUrl の組み合わせごとに `HelloPaySDK` インスタンスをキャッシュ。\n// 加盟店が誤って inline で loadHelloPay を呼び出した場合でも同一 SDK インスタンスが\n// 返るようにして、Elements セッションの BroadcastChannel session ID ミスマッチを防ぐ。\nconst sdkCache = new Map<string, Promise<HelloPay | null>>();\n\n/** テスト専用: module-scope のキャッシュをすべて初期状態に戻す。 */\nexport function __resetLoadPromiseForTest(): void {\n scriptPromiseCache.clear();\n sdkCache.clear();\n}\n\nfunction normalizeSdkUrl(url: string): string {\n if (typeof window === 'undefined') return url;\n\n try {\n return new URL(url, window.location.href).href;\n } catch {\n return url;\n }\n}\n\nfunction normalizeLocale(locale?: string): string {\n return locale || 'ja';\n}\n\nfunction loadScript(url: string): Promise<HelloPayFactory | null> {\n if (typeof window === 'undefined') return Promise.resolve(null);\n\n // URL を正規化してプロトコル省略形などを統一する。\n // テスト / SSR 環境では window.location が http URL でない場合があるため try-catch で保護。\n const normalizedUrl = normalizeSdkUrl(url);\n\n return new Promise((resolve, reject) => {\n // script は URL 単位で管理する。異なる URL の script を削除せず共存させることで、\n // E2E / ローカル開発で異なる sdkUrl が並列で呼ばれても Promise が宙吊りにならない。\n // 状態は dataset.hellopayState に \"loading\" | \"loaded\" | \"error\" で刻む。\n const managedScripts = Array.from(\n document.querySelectorAll<HTMLScriptElement>('script[data-hellopay-sdk]'),\n );\n\n const existing = managedScripts.find((script) => {\n const rawUrl = script.dataset.hellopayUrl || script.src;\n if (!rawUrl) return false;\n try {\n return new URL(rawUrl, window.location.href).href === normalizedUrl;\n } catch {\n return rawUrl === normalizedUrl;\n }\n });\n\n if (existing) {\n const state = existing.dataset.hellopayState;\n if (state === 'loaded') {\n resolve(window.HelloPay ?? null);\n return;\n }\n if (state === 'loading') {\n existing.addEventListener('load', () => resolve(window.HelloPay ?? null), {\n once: true,\n });\n existing.addEventListener(\n 'error',\n () => reject(new Error('HelloPay SDK failed to load')),\n { once: true },\n );\n return;\n }\n // state === \"error\" or undefined: 失敗済み or 未知の状態なので作り直す\n existing.remove();\n }\n\n const script = document.createElement('script');\n script.src = normalizedUrl;\n script.async = true;\n script.dataset.hellopaySdk = 'true';\n script.dataset.hellopayUrl = normalizedUrl; // URL 比較用\n script.dataset.hellopayState = 'loading';\n script.addEventListener(\n 'load',\n () => {\n if (!window.HelloPay) {\n script.dataset.hellopayState = 'error';\n reject(new Error('HelloPay SDK loaded but window.HelloPay is missing'));\n return;\n }\n script.dataset.hellopayState = 'loaded';\n resolve(window.HelloPay);\n },\n { once: true },\n );\n script.addEventListener(\n 'error',\n () => {\n script.dataset.hellopayState = 'error';\n reject(new Error('HelloPay SDK failed to load'));\n },\n { once: true },\n );\n document.head.appendChild(script);\n });\n}\n\nexport interface LoadHelloPayOptions {\n /** SDK 配信 URL(ローカル開発・E2E で差し替える用。省略時は js.hello-pay.app から配信し、test/live は SDK 内部で key prefix から解決) */\n sdkUrl?: string;\n locale?: string;\n}\n\n/**\n * HelloPay SDK をロードして初期化する。\n * モジュールスコープで 1 回だけ呼び、返り値の Promise を `<Elements>` に渡す。\n */\nexport function loadHelloPay(\n publicKey: string,\n options: LoadHelloPayOptions = {},\n): Promise<HelloPay | null> {\n const sdkUrl = normalizeSdkUrl(options.sdkUrl ?? DEFAULT_SDK_URL);\n const locale = normalizeLocale(options.locale);\n const cacheKey = `${publicKey}::${locale}::${sdkUrl}`;\n const cached = sdkCache.get(cacheKey);\n if (cached) return cached;\n\n let scriptPromise = scriptPromiseCache.get(sdkUrl);\n if (!scriptPromise) {\n scriptPromise = loadScript(sdkUrl).catch((err) => {\n // 一時的なロード失敗はキャッシュせず次回呼び出しで再試行可能にする\n scriptPromiseCache.delete(sdkUrl);\n throw err;\n });\n scriptPromiseCache.set(sdkUrl, scriptPromise);\n }\n\n const sdkPromise = scriptPromise\n .then((factory) => {\n if (!factory) return null; // SSR\n return factory(publicKey, { locale: options.locale });\n })\n .catch((err) => {\n // SDK インスタンス生成失敗は sdkCache から退避して次回呼び出しで再試行可能にする\n sdkCache.delete(cacheKey);\n throw err;\n });\n sdkCache.set(cacheKey, sdkPromise);\n return sdkPromise;\n}\n","/**\n * <Elements> Provider.\n *\n * loadHelloPay() の Promise を受け取り、SDK インスタンスと Elements セッションを\n * 子孫に Context 経由で供給する。\n */\n\nimport { createContext, useEffect, useState, type ReactNode } from 'react';\nimport type { HelloPay, Elements as ElementsSession } from './types';\n\nexport interface ElementsContextValue {\n hellopay: HelloPay | null;\n elements: ElementsSession | null;\n}\n\nconst EMPTY_CONTEXT: ElementsContextValue = {\n hellopay: null,\n elements: null,\n};\n\nexport const ElementsContext = createContext<ElementsContextValue>(EMPTY_CONTEXT);\n\nexport interface ElementsProps {\n /** loadHelloPay() の戻り値。モジュールスコープで 1 回だけ生成すること */\n hellopay: Promise<HelloPay | null> | HelloPay | null;\n children: ReactNode;\n}\n\nexport function Elements({ hellopay: hellopayProp, children }: ElementsProps) {\n const [ctx, setCtx] = useState<ElementsContextValue>(EMPTY_CONTEXT);\n\n useEffect(() => {\n let cancelled = false;\n setCtx(EMPTY_CONTEXT);\n\n Promise.resolve(hellopayProp)\n .then((sdk) => {\n if (cancelled) return;\n if (!sdk) {\n setCtx(EMPTY_CONTEXT);\n return;\n }\n setCtx({ hellopay: sdk, elements: sdk.elements() });\n })\n .catch(() => {\n // SDK ロード失敗時は null のまま(unhandled rejection を防ぐ)\n if (cancelled) return;\n setCtx(EMPTY_CONTEXT);\n });\n\n return () => {\n cancelled = true;\n };\n }, [hellopayProp]);\n\n return <ElementsContext.Provider value={ctx}>{children}</ElementsContext.Provider>;\n}\n","/**\n * Element コンポーネント。\n *\n * 各コンポーネントは 1 つの `<div>` コンテナを描画するだけで、\n * 実体(カード情報が入る iframe)はコア SDK が差し込む。\n *\n * React Strict Mode の二重 mount 対応:\n * - effect の cleanup で element.unmount() を必ず呼ぶ\n * - unmount 後は Elements._elements 配列から除外される(コア SDK 側で対応済)\n * - 結果として Strict Mode で effect が再実行されても古い iframe は残らない\n */\n\nimport { useEffect, useRef, useState, type CSSProperties } from 'react';\nimport { useElements } from './hooks';\nimport type {\n ElementType,\n ElementOptions,\n ElementChangeEvent,\n ElementFocusEvent,\n ElementReadyEvent,\n} from './types';\n\nexport interface ElementProps {\n options?: ElementOptions;\n className?: string;\n style?: CSSProperties;\n onReady?: (e: ElementReadyEvent) => void;\n onChange?: (e: ElementChangeEvent) => void;\n onFocus?: (e: ElementFocusEvent) => void;\n onBlur?: (e: ElementFocusEvent) => void;\n}\n\nconst PROBE_PX = 100000;\n\n/**\n * コンテナの幅・高さのうち、中身に追従する(加盟店が寸法を決めていない)軸を返す。\n */\nfunction contentSizedAxes(container: HTMLElement) {\n // display: none の祖先の中ではレイアウトが無いので、computed value で見る。\n if (container.getClientRects().length === 0) {\n const computed = getComputedStyle(container);\n const isAuto = (v: string) => v === '' || v === 'auto';\n return { width: isAuto(computed.width), height: isAuto(computed.height) };\n }\n // computed の height で判定すると、border-box の padding・border や min-height で\n // 0 にならず見誤るので、大きな子を一時的に入れて寸法が伸びるかで確かめる。\n // 祖先の transform(モーダルの scale のアニメーションなど)に左右されないよう、\n // getBoundingClientRect ではなくレイアウト上の寸法の offset* で比べる。\n const before = { width: container.offsetWidth, height: container.offsetHeight };\n const probe = container.ownerDocument.createElement('div');\n probe.style.cssText = `width: ${PROBE_PX}px; height: ${PROBE_PX}px;`;\n container.appendChild(probe);\n const grows = {\n width: container.offsetWidth > before.width,\n height: container.offsetHeight > before.height,\n };\n probe.remove();\n return grows;\n}\n\n/**\n * 指定 type の Element を管理する React コンポーネントを生成する。\n */\nfunction createElementComponent(type: ElementType) {\n function ElementComponent({\n options,\n className,\n style,\n onReady,\n onChange,\n onFocus,\n onBlur,\n }: ElementProps) {\n const elements = useElements();\n const containerRef = useRef<HTMLDivElement | null>(null);\n const [fill, setFill] = useState({ width: false, height: false });\n\n // 最新のハンドラを ref で保持(effect を毎回 re-run させないため)\n const handlersRef = useRef({ onReady, onChange, onFocus, onBlur });\n handlersRef.current = { onReady, onChange, onFocus, onBlur };\n\n useEffect(() => {\n const container = containerRef.current;\n if (!elements || !container) return;\n\n // 100% を当てた後に測り直すと親の寸法に追従して見えるので、立てた軸は戻さない。\n const unset = contentSizedAxes(container);\n setFill((prev) => ({\n width: prev.width || unset.width,\n height: prev.height || unset.height,\n }));\n\n const element = elements.create(type, options);\n element.mount(container);\n\n const unsubs = [\n element.on('ready', (e) => handlersRef.current.onReady?.(e)),\n element.on('change', (e) => handlersRef.current.onChange?.(e)),\n element.on('focus', (e) => handlersRef.current.onFocus?.(e)),\n element.on('blur', (e) => handlersRef.current.onBlur?.(e)),\n ];\n\n return () => {\n unsubs.forEach((u) => u());\n element.unmount();\n };\n // options はマウント時にしか反映しない方針(Stripe と同じ挙動)。\n // 動的に style を変えたい場合は後続 PR で element.update() を追加。\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [elements]);\n\n // インラインの既定値は className の寸法に勝つので、加盟店が決めていない軸だけに付ける。\n // 判定は mount 時の 1 回だけなので、後から寸法を変えるなら style で渡す。\n return (\n <div\n ref={containerRef}\n className={className}\n style={{\n ...(fill.width && { width: '100%' }),\n ...(fill.height && { height: '100%' }),\n ...style,\n }}\n />\n );\n }\n ElementComponent.displayName = `HelloPay(${type})`;\n // getElement(CardNumberElement) のようにコンポーネント自身を型指定子として使えるよう、\n // 静的プロパティに type を生やしておく\n ElementComponent.__elementType = type as ElementType;\n return ElementComponent;\n}\n\nexport const CardNumberElement = createElementComponent('cardNumber');\nexport const CardExpiryElement = createElementComponent('cardExpiry');\nexport const CardCvcElement = createElementComponent('cardCvc');\nexport const CardholderNameElement = createElementComponent('cardholderName');\n// 韓国発行カードの Toss 登録に必要な暗証番号の先頭 2 桁。表示・非表示は\n// 発行国の判定結果に応じて呼び出し側が mount / unmount で制御する。\nexport const CardPasswordElement = createElementComponent('cardPassword');\n\nexport type ElementComponent = typeof CardNumberElement;\n","/**\n * React hooks.\n *\n * useHelloPay / useElements は <Elements> Provider の Context から\n * SDK インスタンスと Elements セッションを取り出す。\n * SDK ロード完了までは null を返すので、ハンドラ内で null チェックする。\n */\n\nimport { useContext } from 'react';\nimport { ElementsContext } from './Elements';\nimport type { HelloPay, Elements } from './types';\n\nexport function useHelloPay(): HelloPay | null {\n return useContext(ElementsContext).hellopay;\n}\n\nexport function useElements(): Elements | null {\n return useContext(ElementsContext).elements;\n}\n","/**\n * Apple Pay ボタン。\n *\n * `useHelloPay()` から SDK を取り、対応端末(Safari + Apple Pay 利用可能)でのみ\n * Apple 公式スタイルのボタンを描画する。非対応端末では何も描画しない。\n *\n * click で `hellopay.applePay()` を同期呼び出しし(`new ApplePaySession` の\n * user gesture 要件のため)、結果を `onToken` / `onError` / `onCancel` に振り分ける。\n * SDK は Apple Pay トークンを返すだけで課金はしないので、加盟店は `onToken` で\n * 受けたトークンを自社バックエンドから決済 API に送って課金する。\n */\n\nimport { useEffect, useRef, useState, type CSSProperties } from 'react';\nimport { useHelloPay } from './hooks';\nimport type { ApplePayCard, ApplePayOptions, HelloPayError } from './types';\n\nexport interface ApplePayButtonProps {\n options: ApplePayOptions;\n /** トークン取得成功時。トークンを自社バックエンド経由で決済 API に送る。 */\n onToken: (token: string, card: ApplePayCard) => void;\n /** validation / network エラー時。 */\n onError?: (error: HelloPayError) => void;\n /** ユーザーが Apple Pay シートを閉じた時。 */\n onCancel?: () => void;\n /** Apple Pay ボタンの色。既定 `black`。 */\n buttonStyle?: 'black' | 'white' | 'white-outline';\n /** Apple Pay ボタンの種別(`plain` / `buy` / `pay` 等)。既定 `plain`。 */\n buttonType?: string;\n className?: string;\n style?: CSSProperties;\n}\n\nexport function ApplePayButton({\n options,\n onToken,\n onError,\n onCancel,\n buttonStyle = 'black',\n buttonType = 'plain',\n className,\n style,\n}: ApplePayButtonProps) {\n const hellopay = useHelloPay();\n const buttonRef = useRef<HTMLButtonElement | null>(null);\n const [supported, setSupported] = useState(false);\n\n // canMakeApplePayPayments は ApplePaySession 依存で client-only のため effect で判定。\n useEffect(() => {\n setSupported(!!hellopay && hellopay.canMakeApplePayPayments());\n }, [hellopay]);\n\n // Apple Pay ボタンの見た目はベンダー CSS プロパティで指定する。React の style 型に\n // 無いため ref 経由で setProperty する。\n useEffect(() => {\n const el = buttonRef.current;\n if (!el) return;\n el.style.setProperty('-webkit-appearance', '-apple-pay-button');\n el.style.setProperty('-apple-pay-button-type', buttonType);\n el.style.setProperty('-apple-pay-button-style', buttonStyle);\n }, [supported, buttonType, buttonStyle]);\n\n // 最新のハンドラ / options を ref に保持し、click ハンドラを毎回作り直さない。\n const latest = useRef({ options, onToken, onError, onCancel, hellopay });\n latest.current = { options, onToken, onError, onCancel, hellopay };\n\n if (!supported) return null;\n\n const handleClick = () => {\n const current = latest.current;\n if (!current.hellopay) return;\n // user gesture 要件のため click ハンドラから同期呼び出しする。\n current.hellopay\n .applePay(current.options)\n .then((result) => {\n if (result.error) {\n if (result.error.type === 'cancel') {\n current.onCancel?.();\n } else {\n current.onError?.(result.error);\n }\n return;\n }\n current.onToken(result.token, result.card);\n })\n .catch((err: unknown) => {\n current.onError?.({\n type: 'config_error',\n code: 'apple_pay_error',\n message: err instanceof Error ? err.message : 'Apple Pay error',\n });\n });\n };\n\n return (\n <button\n ref={buttonRef}\n type=\"button\"\n onClick={handleClick}\n className={className}\n style={{ width: '100%', height: '44px', cursor: 'pointer', ...style }}\n />\n );\n}\n\nApplePayButton.displayName = 'HelloPay(ApplePayButton)';\n"],"mappings":"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","names":["script","import_react","import_react","import_jsx_runtime","import_react","import_jsx_runtime"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -9,7 +9,7 @@ import { ReactNode, CSSProperties } from 'react';
|
|
|
9
9
|
* HelloPay 管理ドメインから配信)を満たすため。
|
|
10
10
|
* npm パッケージは型定義とローダーのみを提供する。
|
|
11
11
|
*/
|
|
12
|
-
type ElementType = 'cardNumber' | 'cardExpiry' | 'cardCvc' | 'cardholderName';
|
|
12
|
+
type ElementType = 'cardNumber' | 'cardExpiry' | 'cardCvc' | 'cardholderName' | 'cardPassword';
|
|
13
13
|
interface ElementStyle {
|
|
14
14
|
base?: {
|
|
15
15
|
color?: string;
|
|
@@ -29,12 +29,45 @@ interface ElementStyle {
|
|
|
29
29
|
interface ElementOptions {
|
|
30
30
|
style?: ElementStyle;
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
/**
|
|
33
|
+
* カード発行国の判定結果。
|
|
34
|
+
*
|
|
35
|
+
* `null` を使わず 3 状態で返すのは、呼び出し側の挙動が異なるため。
|
|
36
|
+
* `pending` はフォームを組み替えずに現状の表示を維持し、`unknown` は
|
|
37
|
+
* どの発行国グループにも該当しないもの(`default`)として組み替える。
|
|
38
|
+
*
|
|
39
|
+
* 現在 `determined` になるのは韓国の国内専用カード(先頭 4 桁 `9410`)のみ。
|
|
40
|
+
* それ以外の発行国と、韓国発行の国際ブランドカードは `unknown` を返す。
|
|
41
|
+
* 誤検知を避けるため確実に判定できる範囲だけに絞っており、対応範囲は
|
|
42
|
+
* 今後 `country` を増やす形で広げる。
|
|
43
|
+
*/
|
|
44
|
+
type IssuerCountryResult = {
|
|
45
|
+
status: 'pending';
|
|
46
|
+
} | {
|
|
47
|
+
status: 'unknown';
|
|
48
|
+
} | {
|
|
49
|
+
status: 'determined';
|
|
50
|
+
country: string;
|
|
51
|
+
};
|
|
52
|
+
interface CardNumberChangeEvent {
|
|
53
|
+
elementType: 'cardNumber';
|
|
34
54
|
empty: boolean;
|
|
35
55
|
complete: boolean;
|
|
36
56
|
brand?: string | null;
|
|
57
|
+
issuerCountry: IssuerCountryResult;
|
|
37
58
|
}
|
|
59
|
+
interface OtherElementChangeEvent {
|
|
60
|
+
elementType: Exclude<ElementType, 'cardNumber'>;
|
|
61
|
+
empty: boolean;
|
|
62
|
+
complete: boolean;
|
|
63
|
+
brand?: string | null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* `elementType` で分岐する判別可能ユニオン。`issuerCountry` は cardNumber
|
|
67
|
+
* にしか存在しないため、読むには `elementType === 'cardNumber'` での
|
|
68
|
+
* narrowing が必要。
|
|
69
|
+
*/
|
|
70
|
+
type ElementChangeEvent = CardNumberChangeEvent | OtherElementChangeEvent;
|
|
38
71
|
interface ElementFocusEvent {
|
|
39
72
|
elementType: ElementType;
|
|
40
73
|
}
|
|
@@ -70,28 +103,163 @@ interface HelloPayError {
|
|
|
70
103
|
code: string;
|
|
71
104
|
message: string;
|
|
72
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* トークンの発行経路。`'hellopay'` は従来どおり HelloPay の決済 API に渡す。
|
|
108
|
+
*
|
|
109
|
+
* `'stripe'` / `'gmo'` は Mastercard 審査完了までの暫定措置で、どちらを使うかは
|
|
110
|
+
* プロジェクトの設定で決まる。いずれも加盟店自身の PSP 契約上に発行された
|
|
111
|
+
* トークンのため、決済・3DS・返金は加盟店自身の実装で行う。
|
|
112
|
+
*
|
|
113
|
+
* - `'stripe'`: 加盟店自身の Stripe アカウント上のトークン
|
|
114
|
+
* - `'gmo'`: 加盟店自身の GMO PGマルチペイメント契約上のトークン。決済は
|
|
115
|
+
* `ExecTran` に `Token` と `TokenType=1` を渡す。有効期限は
|
|
116
|
+
* {@link CreateTokenResult.tokenExpiryDate} を参照
|
|
117
|
+
*/
|
|
118
|
+
type TokenProvider = 'hellopay' | 'stripe' | 'gmo';
|
|
73
119
|
/**
|
|
74
120
|
* createToken の返り値。Discriminated Union により、success 時は
|
|
75
|
-
* token/card のみ、error 時は error のみが型として存在する。
|
|
121
|
+
* token/provider/card のみ、error 時は error のみが型として存在する。
|
|
76
122
|
*
|
|
77
123
|
* ```ts
|
|
78
|
-
* const { token, card, error } = await hellopay.createToken(el);
|
|
124
|
+
* const { token, provider, card, error } = await hellopay.createToken(el);
|
|
79
125
|
* if (error) return showError(error.message);
|
|
80
|
-
* sendToServer(token, card);
|
|
126
|
+
* sendToServer(token, provider, card);
|
|
81
127
|
* ```
|
|
82
128
|
*/
|
|
83
129
|
type CreateTokenResult = {
|
|
84
130
|
token: string;
|
|
131
|
+
provider: TokenProvider;
|
|
85
132
|
card: TokenCard;
|
|
133
|
+
/**
|
|
134
|
+
* トークンの有効期限(RFC3339)。寿命が経路ごとに異なる provider でのみ
|
|
135
|
+
* 返る。現状は `'gmo'`(発行から 30 分・1 回限り)のみ。
|
|
136
|
+
*/
|
|
137
|
+
tokenExpiryDate?: string;
|
|
86
138
|
error?: undefined;
|
|
87
139
|
} | {
|
|
88
140
|
token?: undefined;
|
|
141
|
+
provider?: undefined;
|
|
89
142
|
card?: undefined;
|
|
143
|
+
tokenExpiryDate?: undefined;
|
|
90
144
|
error: HelloPayError;
|
|
91
145
|
};
|
|
146
|
+
/**
|
|
147
|
+
* 複数プロバイダへのカード登録 (#2747) の返り値。
|
|
148
|
+
*
|
|
149
|
+
* `CreateTokenResult` と違い **トークンを持たない** — 登録結果 (Toss の BillingKey など) は
|
|
150
|
+
* 加盟店バックエンドがサーバ間 API で取得する。判別は `status` で行う。
|
|
151
|
+
*
|
|
152
|
+
* ```ts
|
|
153
|
+
* const result = await hellopay.createToken(el, { sessionClientSecret });
|
|
154
|
+
* if (result.status === 'failed') {
|
|
155
|
+
* // 発行国の申告と BIN 判定が食い違った場合はフォームを組み替える
|
|
156
|
+
* if (result.error.code === 'issuer_country_mismatch') {
|
|
157
|
+
* rebuildForm(result.error.issuerCountryGroup);
|
|
158
|
+
* }
|
|
159
|
+
* return showError(result.error.message);
|
|
160
|
+
* }
|
|
161
|
+
* showRegisteredCard(result.card);
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
type CardRegistrationResult = {
|
|
165
|
+
status: 'succeeded';
|
|
166
|
+
card: CardRegistrationCard | null;
|
|
167
|
+
error?: undefined;
|
|
168
|
+
} | {
|
|
169
|
+
status: 'failed';
|
|
170
|
+
card: null;
|
|
171
|
+
error: CardRegistrationError;
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* 登録できたカードのメタデータ。
|
|
175
|
+
*
|
|
176
|
+
* `brand` が `TokenCard` と違って `null` を許すのは、韓国の国内専用カード (`9410` 始まり) が
|
|
177
|
+
* 国際ブランドを持たないため。サーバーの BIN 判定がブランドを確定できないとそのまま `null` で
|
|
178
|
+
* 返るので、型でも表す。
|
|
179
|
+
*/
|
|
180
|
+
interface CardRegistrationCard {
|
|
181
|
+
brand: string | null;
|
|
182
|
+
last4: string;
|
|
183
|
+
expMonth: string;
|
|
184
|
+
expYear: string;
|
|
185
|
+
}
|
|
186
|
+
interface CardRegistrationError extends HelloPayError {
|
|
187
|
+
/**
|
|
188
|
+
* `issuer_country_mismatch` のときだけ入る、BIN 判定で確定した発行国グループ
|
|
189
|
+
* (該当なしは `'default'`)。呼び出し側はこれを使って入力フォームを組み替える。
|
|
190
|
+
*/
|
|
191
|
+
issuerCountryGroup?: string;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Apple Pay で決済したカードのメタデータ。Apple Pay は client 側で last4 /
|
|
195
|
+
* 有効期限を確実に取得できないため、カード決済の `TokenCard` とは別形状。
|
|
196
|
+
*/
|
|
197
|
+
interface ApplePayCard {
|
|
198
|
+
/** カードネットワーク(例: `visa` / `masterCard`)。取得不能時は `unknown`。 */
|
|
199
|
+
brand: string;
|
|
200
|
+
/** Wallet 上の表示名(例: `Visa 1234`)。 */
|
|
201
|
+
displayName: string;
|
|
202
|
+
}
|
|
203
|
+
interface ApplePayOptions {
|
|
204
|
+
/** 請求金額(JPY・整数)。 */
|
|
205
|
+
amount: number;
|
|
206
|
+
/** Apple Pay シートに表示する店舗名。省略時は `HelloPay`。 */
|
|
207
|
+
label?: string;
|
|
208
|
+
/** 既定 `JPY`。 */
|
|
209
|
+
currencyCode?: string;
|
|
210
|
+
/** 既定 `JP`。 */
|
|
211
|
+
countryCode?: string;
|
|
212
|
+
/** 既定 `['visa', 'masterCard', 'amex', 'jcb']`。 */
|
|
213
|
+
supportedNetworks?: string[];
|
|
214
|
+
/** 既定 `['supports3DS']`。 */
|
|
215
|
+
merchantCapabilities?: string[];
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* ユーザーが Apple Pay シートを閉じたときのキャンセル。`cancel` は Apple Pay
|
|
219
|
+
* 固有の結果なので、`createToken` と共有する `HelloPayError` には載せず
|
|
220
|
+
* Apple Pay 専用の union(`ApplePayError`)でのみ表現する。これにより
|
|
221
|
+
* `createToken` 利用者の網羅的 `switch` を後方互換に保つ。
|
|
222
|
+
*/
|
|
223
|
+
interface ApplePayCancelError {
|
|
224
|
+
type: 'cancel';
|
|
225
|
+
code: 'cancelled';
|
|
226
|
+
message: string;
|
|
227
|
+
}
|
|
228
|
+
type ApplePayError = HelloPayError | ApplePayCancelError;
|
|
229
|
+
/**
|
|
230
|
+
* `applePay` の返り値。`createToken` と同型の discriminated union。
|
|
231
|
+
* 成功時は Apple Pay トークン(`token.paymentData` の JSON 文字列)を返し、
|
|
232
|
+
* 加盟店はこれを自社バックエンドから決済 API に送って課金する。
|
|
233
|
+
* キャンセルは `error.type === 'cancel'` として失敗側に載る。
|
|
234
|
+
*/
|
|
235
|
+
type ApplePayResult = {
|
|
236
|
+
token: string;
|
|
237
|
+
provider: 'applepay';
|
|
238
|
+
card: ApplePayCard;
|
|
239
|
+
error?: undefined;
|
|
240
|
+
} | {
|
|
241
|
+
token?: undefined;
|
|
242
|
+
provider?: undefined;
|
|
243
|
+
card?: undefined;
|
|
244
|
+
error: ApplePayError;
|
|
245
|
+
};
|
|
92
246
|
interface HelloPay {
|
|
93
247
|
elements(): Elements$1;
|
|
94
248
|
createToken(cardNumberElement: Element): Promise<CreateTokenResult>;
|
|
249
|
+
/**
|
|
250
|
+
* 登録セッション経路 (#2747)。`sessionClientSecret` を渡すと、加盟店バックエンドが
|
|
251
|
+
* 宣言した複数の決済プロバイダへまとめてカードを登録する。
|
|
252
|
+
*
|
|
253
|
+
* **トークンは返らない。** BillingKey も PSP のトークンもブラウザには渡らず、
|
|
254
|
+
* 加盟店バックエンドがサーバ間 API で取得する。
|
|
255
|
+
*/
|
|
256
|
+
createToken(cardNumberElement: Element, options: {
|
|
257
|
+
sessionClientSecret: string;
|
|
258
|
+
}): Promise<CardRegistrationResult>;
|
|
259
|
+
/** 対応端末(Safari + Apple Pay 利用可能)でのみ true。 */
|
|
260
|
+
canMakeApplePayPayments(): boolean;
|
|
261
|
+
/** Apple Pay シートを開き、都度課金トークンを取得する。 */
|
|
262
|
+
applePay(options: ApplePayOptions): Promise<ApplePayResult>;
|
|
95
263
|
}
|
|
96
264
|
type HelloPayFactory = (publicKey: string, options?: {
|
|
97
265
|
locale?: string;
|
|
@@ -166,6 +334,31 @@ declare const CardholderNameElement: {
|
|
|
166
334
|
displayName: string;
|
|
167
335
|
__elementType: ElementType;
|
|
168
336
|
};
|
|
337
|
+
declare const CardPasswordElement: {
|
|
338
|
+
({ options, className, style, onReady, onChange, onFocus, onBlur, }: ElementProps): react_jsx_runtime.JSX.Element;
|
|
339
|
+
displayName: string;
|
|
340
|
+
__elementType: ElementType;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
interface ApplePayButtonProps {
|
|
344
|
+
options: ApplePayOptions;
|
|
345
|
+
/** トークン取得成功時。トークンを自社バックエンド経由で決済 API に送る。 */
|
|
346
|
+
onToken: (token: string, card: ApplePayCard) => void;
|
|
347
|
+
/** validation / network エラー時。 */
|
|
348
|
+
onError?: (error: HelloPayError) => void;
|
|
349
|
+
/** ユーザーが Apple Pay シートを閉じた時。 */
|
|
350
|
+
onCancel?: () => void;
|
|
351
|
+
/** Apple Pay ボタンの色。既定 `black`。 */
|
|
352
|
+
buttonStyle?: 'black' | 'white' | 'white-outline';
|
|
353
|
+
/** Apple Pay ボタンの種別(`plain` / `buy` / `pay` 等)。既定 `plain`。 */
|
|
354
|
+
buttonType?: string;
|
|
355
|
+
className?: string;
|
|
356
|
+
style?: CSSProperties;
|
|
357
|
+
}
|
|
358
|
+
declare function ApplePayButton({ options, onToken, onError, onCancel, buttonStyle, buttonType, className, style, }: ApplePayButtonProps): react_jsx_runtime.JSX.Element | null;
|
|
359
|
+
declare namespace ApplePayButton {
|
|
360
|
+
var displayName: string;
|
|
361
|
+
}
|
|
169
362
|
|
|
170
363
|
/**
|
|
171
364
|
* React hooks.
|
|
@@ -178,4 +371,4 @@ declare const CardholderNameElement: {
|
|
|
178
371
|
declare function useHelloPay(): HelloPay | null;
|
|
179
372
|
declare function useElements(): Elements$1 | null;
|
|
180
373
|
|
|
181
|
-
export { CardCvcElement, CardExpiryElement, CardNumberElement, CardholderNameElement, type CreateTokenResult, type Element, type ElementChangeEvent, type ElementFocusEvent, type ElementOptions, type ElementProps, type ElementReadyEvent, type ElementStyle, type ElementType, Elements, type ElementsProps, type Elements$1 as ElementsSession, type HelloPay, type HelloPayError, type LoadHelloPayOptions, type TokenCard, type Unsubscribe, loadHelloPay, useElements, useHelloPay };
|
|
374
|
+
export { ApplePayButton, type ApplePayButtonProps, type ApplePayCancelError, type ApplePayCard, type ApplePayError, type ApplePayOptions, type ApplePayResult, CardCvcElement, CardExpiryElement, type CardNumberChangeEvent, CardNumberElement, CardPasswordElement, CardholderNameElement, type CreateTokenResult, type Element, type ElementChangeEvent, type ElementFocusEvent, type ElementOptions, type ElementProps, type ElementReadyEvent, type ElementStyle, type ElementType, Elements, type ElementsProps, type Elements$1 as ElementsSession, type HelloPay, type HelloPayError, type IssuerCountryResult, type LoadHelloPayOptions, type OtherElementChangeEvent, type TokenCard, type TokenProvider, type Unsubscribe, loadHelloPay, useElements, useHelloPay };
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { ReactNode, CSSProperties } from 'react';
|
|
|
9
9
|
* HelloPay 管理ドメインから配信)を満たすため。
|
|
10
10
|
* npm パッケージは型定義とローダーのみを提供する。
|
|
11
11
|
*/
|
|
12
|
-
type ElementType = 'cardNumber' | 'cardExpiry' | 'cardCvc' | 'cardholderName';
|
|
12
|
+
type ElementType = 'cardNumber' | 'cardExpiry' | 'cardCvc' | 'cardholderName' | 'cardPassword';
|
|
13
13
|
interface ElementStyle {
|
|
14
14
|
base?: {
|
|
15
15
|
color?: string;
|
|
@@ -29,12 +29,45 @@ interface ElementStyle {
|
|
|
29
29
|
interface ElementOptions {
|
|
30
30
|
style?: ElementStyle;
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
/**
|
|
33
|
+
* カード発行国の判定結果。
|
|
34
|
+
*
|
|
35
|
+
* `null` を使わず 3 状態で返すのは、呼び出し側の挙動が異なるため。
|
|
36
|
+
* `pending` はフォームを組み替えずに現状の表示を維持し、`unknown` は
|
|
37
|
+
* どの発行国グループにも該当しないもの(`default`)として組み替える。
|
|
38
|
+
*
|
|
39
|
+
* 現在 `determined` になるのは韓国の国内専用カード(先頭 4 桁 `9410`)のみ。
|
|
40
|
+
* それ以外の発行国と、韓国発行の国際ブランドカードは `unknown` を返す。
|
|
41
|
+
* 誤検知を避けるため確実に判定できる範囲だけに絞っており、対応範囲は
|
|
42
|
+
* 今後 `country` を増やす形で広げる。
|
|
43
|
+
*/
|
|
44
|
+
type IssuerCountryResult = {
|
|
45
|
+
status: 'pending';
|
|
46
|
+
} | {
|
|
47
|
+
status: 'unknown';
|
|
48
|
+
} | {
|
|
49
|
+
status: 'determined';
|
|
50
|
+
country: string;
|
|
51
|
+
};
|
|
52
|
+
interface CardNumberChangeEvent {
|
|
53
|
+
elementType: 'cardNumber';
|
|
34
54
|
empty: boolean;
|
|
35
55
|
complete: boolean;
|
|
36
56
|
brand?: string | null;
|
|
57
|
+
issuerCountry: IssuerCountryResult;
|
|
37
58
|
}
|
|
59
|
+
interface OtherElementChangeEvent {
|
|
60
|
+
elementType: Exclude<ElementType, 'cardNumber'>;
|
|
61
|
+
empty: boolean;
|
|
62
|
+
complete: boolean;
|
|
63
|
+
brand?: string | null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* `elementType` で分岐する判別可能ユニオン。`issuerCountry` は cardNumber
|
|
67
|
+
* にしか存在しないため、読むには `elementType === 'cardNumber'` での
|
|
68
|
+
* narrowing が必要。
|
|
69
|
+
*/
|
|
70
|
+
type ElementChangeEvent = CardNumberChangeEvent | OtherElementChangeEvent;
|
|
38
71
|
interface ElementFocusEvent {
|
|
39
72
|
elementType: ElementType;
|
|
40
73
|
}
|
|
@@ -70,28 +103,163 @@ interface HelloPayError {
|
|
|
70
103
|
code: string;
|
|
71
104
|
message: string;
|
|
72
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* トークンの発行経路。`'hellopay'` は従来どおり HelloPay の決済 API に渡す。
|
|
108
|
+
*
|
|
109
|
+
* `'stripe'` / `'gmo'` は Mastercard 審査完了までの暫定措置で、どちらを使うかは
|
|
110
|
+
* プロジェクトの設定で決まる。いずれも加盟店自身の PSP 契約上に発行された
|
|
111
|
+
* トークンのため、決済・3DS・返金は加盟店自身の実装で行う。
|
|
112
|
+
*
|
|
113
|
+
* - `'stripe'`: 加盟店自身の Stripe アカウント上のトークン
|
|
114
|
+
* - `'gmo'`: 加盟店自身の GMO PGマルチペイメント契約上のトークン。決済は
|
|
115
|
+
* `ExecTran` に `Token` と `TokenType=1` を渡す。有効期限は
|
|
116
|
+
* {@link CreateTokenResult.tokenExpiryDate} を参照
|
|
117
|
+
*/
|
|
118
|
+
type TokenProvider = 'hellopay' | 'stripe' | 'gmo';
|
|
73
119
|
/**
|
|
74
120
|
* createToken の返り値。Discriminated Union により、success 時は
|
|
75
|
-
* token/card のみ、error 時は error のみが型として存在する。
|
|
121
|
+
* token/provider/card のみ、error 時は error のみが型として存在する。
|
|
76
122
|
*
|
|
77
123
|
* ```ts
|
|
78
|
-
* const { token, card, error } = await hellopay.createToken(el);
|
|
124
|
+
* const { token, provider, card, error } = await hellopay.createToken(el);
|
|
79
125
|
* if (error) return showError(error.message);
|
|
80
|
-
* sendToServer(token, card);
|
|
126
|
+
* sendToServer(token, provider, card);
|
|
81
127
|
* ```
|
|
82
128
|
*/
|
|
83
129
|
type CreateTokenResult = {
|
|
84
130
|
token: string;
|
|
131
|
+
provider: TokenProvider;
|
|
85
132
|
card: TokenCard;
|
|
133
|
+
/**
|
|
134
|
+
* トークンの有効期限(RFC3339)。寿命が経路ごとに異なる provider でのみ
|
|
135
|
+
* 返る。現状は `'gmo'`(発行から 30 分・1 回限り)のみ。
|
|
136
|
+
*/
|
|
137
|
+
tokenExpiryDate?: string;
|
|
86
138
|
error?: undefined;
|
|
87
139
|
} | {
|
|
88
140
|
token?: undefined;
|
|
141
|
+
provider?: undefined;
|
|
89
142
|
card?: undefined;
|
|
143
|
+
tokenExpiryDate?: undefined;
|
|
90
144
|
error: HelloPayError;
|
|
91
145
|
};
|
|
146
|
+
/**
|
|
147
|
+
* 複数プロバイダへのカード登録 (#2747) の返り値。
|
|
148
|
+
*
|
|
149
|
+
* `CreateTokenResult` と違い **トークンを持たない** — 登録結果 (Toss の BillingKey など) は
|
|
150
|
+
* 加盟店バックエンドがサーバ間 API で取得する。判別は `status` で行う。
|
|
151
|
+
*
|
|
152
|
+
* ```ts
|
|
153
|
+
* const result = await hellopay.createToken(el, { sessionClientSecret });
|
|
154
|
+
* if (result.status === 'failed') {
|
|
155
|
+
* // 発行国の申告と BIN 判定が食い違った場合はフォームを組み替える
|
|
156
|
+
* if (result.error.code === 'issuer_country_mismatch') {
|
|
157
|
+
* rebuildForm(result.error.issuerCountryGroup);
|
|
158
|
+
* }
|
|
159
|
+
* return showError(result.error.message);
|
|
160
|
+
* }
|
|
161
|
+
* showRegisteredCard(result.card);
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
type CardRegistrationResult = {
|
|
165
|
+
status: 'succeeded';
|
|
166
|
+
card: CardRegistrationCard | null;
|
|
167
|
+
error?: undefined;
|
|
168
|
+
} | {
|
|
169
|
+
status: 'failed';
|
|
170
|
+
card: null;
|
|
171
|
+
error: CardRegistrationError;
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* 登録できたカードのメタデータ。
|
|
175
|
+
*
|
|
176
|
+
* `brand` が `TokenCard` と違って `null` を許すのは、韓国の国内専用カード (`9410` 始まり) が
|
|
177
|
+
* 国際ブランドを持たないため。サーバーの BIN 判定がブランドを確定できないとそのまま `null` で
|
|
178
|
+
* 返るので、型でも表す。
|
|
179
|
+
*/
|
|
180
|
+
interface CardRegistrationCard {
|
|
181
|
+
brand: string | null;
|
|
182
|
+
last4: string;
|
|
183
|
+
expMonth: string;
|
|
184
|
+
expYear: string;
|
|
185
|
+
}
|
|
186
|
+
interface CardRegistrationError extends HelloPayError {
|
|
187
|
+
/**
|
|
188
|
+
* `issuer_country_mismatch` のときだけ入る、BIN 判定で確定した発行国グループ
|
|
189
|
+
* (該当なしは `'default'`)。呼び出し側はこれを使って入力フォームを組み替える。
|
|
190
|
+
*/
|
|
191
|
+
issuerCountryGroup?: string;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Apple Pay で決済したカードのメタデータ。Apple Pay は client 側で last4 /
|
|
195
|
+
* 有効期限を確実に取得できないため、カード決済の `TokenCard` とは別形状。
|
|
196
|
+
*/
|
|
197
|
+
interface ApplePayCard {
|
|
198
|
+
/** カードネットワーク(例: `visa` / `masterCard`)。取得不能時は `unknown`。 */
|
|
199
|
+
brand: string;
|
|
200
|
+
/** Wallet 上の表示名(例: `Visa 1234`)。 */
|
|
201
|
+
displayName: string;
|
|
202
|
+
}
|
|
203
|
+
interface ApplePayOptions {
|
|
204
|
+
/** 請求金額(JPY・整数)。 */
|
|
205
|
+
amount: number;
|
|
206
|
+
/** Apple Pay シートに表示する店舗名。省略時は `HelloPay`。 */
|
|
207
|
+
label?: string;
|
|
208
|
+
/** 既定 `JPY`。 */
|
|
209
|
+
currencyCode?: string;
|
|
210
|
+
/** 既定 `JP`。 */
|
|
211
|
+
countryCode?: string;
|
|
212
|
+
/** 既定 `['visa', 'masterCard', 'amex', 'jcb']`。 */
|
|
213
|
+
supportedNetworks?: string[];
|
|
214
|
+
/** 既定 `['supports3DS']`。 */
|
|
215
|
+
merchantCapabilities?: string[];
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* ユーザーが Apple Pay シートを閉じたときのキャンセル。`cancel` は Apple Pay
|
|
219
|
+
* 固有の結果なので、`createToken` と共有する `HelloPayError` には載せず
|
|
220
|
+
* Apple Pay 専用の union(`ApplePayError`)でのみ表現する。これにより
|
|
221
|
+
* `createToken` 利用者の網羅的 `switch` を後方互換に保つ。
|
|
222
|
+
*/
|
|
223
|
+
interface ApplePayCancelError {
|
|
224
|
+
type: 'cancel';
|
|
225
|
+
code: 'cancelled';
|
|
226
|
+
message: string;
|
|
227
|
+
}
|
|
228
|
+
type ApplePayError = HelloPayError | ApplePayCancelError;
|
|
229
|
+
/**
|
|
230
|
+
* `applePay` の返り値。`createToken` と同型の discriminated union。
|
|
231
|
+
* 成功時は Apple Pay トークン(`token.paymentData` の JSON 文字列)を返し、
|
|
232
|
+
* 加盟店はこれを自社バックエンドから決済 API に送って課金する。
|
|
233
|
+
* キャンセルは `error.type === 'cancel'` として失敗側に載る。
|
|
234
|
+
*/
|
|
235
|
+
type ApplePayResult = {
|
|
236
|
+
token: string;
|
|
237
|
+
provider: 'applepay';
|
|
238
|
+
card: ApplePayCard;
|
|
239
|
+
error?: undefined;
|
|
240
|
+
} | {
|
|
241
|
+
token?: undefined;
|
|
242
|
+
provider?: undefined;
|
|
243
|
+
card?: undefined;
|
|
244
|
+
error: ApplePayError;
|
|
245
|
+
};
|
|
92
246
|
interface HelloPay {
|
|
93
247
|
elements(): Elements$1;
|
|
94
248
|
createToken(cardNumberElement: Element): Promise<CreateTokenResult>;
|
|
249
|
+
/**
|
|
250
|
+
* 登録セッション経路 (#2747)。`sessionClientSecret` を渡すと、加盟店バックエンドが
|
|
251
|
+
* 宣言した複数の決済プロバイダへまとめてカードを登録する。
|
|
252
|
+
*
|
|
253
|
+
* **トークンは返らない。** BillingKey も PSP のトークンもブラウザには渡らず、
|
|
254
|
+
* 加盟店バックエンドがサーバ間 API で取得する。
|
|
255
|
+
*/
|
|
256
|
+
createToken(cardNumberElement: Element, options: {
|
|
257
|
+
sessionClientSecret: string;
|
|
258
|
+
}): Promise<CardRegistrationResult>;
|
|
259
|
+
/** 対応端末(Safari + Apple Pay 利用可能)でのみ true。 */
|
|
260
|
+
canMakeApplePayPayments(): boolean;
|
|
261
|
+
/** Apple Pay シートを開き、都度課金トークンを取得する。 */
|
|
262
|
+
applePay(options: ApplePayOptions): Promise<ApplePayResult>;
|
|
95
263
|
}
|
|
96
264
|
type HelloPayFactory = (publicKey: string, options?: {
|
|
97
265
|
locale?: string;
|
|
@@ -166,6 +334,31 @@ declare const CardholderNameElement: {
|
|
|
166
334
|
displayName: string;
|
|
167
335
|
__elementType: ElementType;
|
|
168
336
|
};
|
|
337
|
+
declare const CardPasswordElement: {
|
|
338
|
+
({ options, className, style, onReady, onChange, onFocus, onBlur, }: ElementProps): react_jsx_runtime.JSX.Element;
|
|
339
|
+
displayName: string;
|
|
340
|
+
__elementType: ElementType;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
interface ApplePayButtonProps {
|
|
344
|
+
options: ApplePayOptions;
|
|
345
|
+
/** トークン取得成功時。トークンを自社バックエンド経由で決済 API に送る。 */
|
|
346
|
+
onToken: (token: string, card: ApplePayCard) => void;
|
|
347
|
+
/** validation / network エラー時。 */
|
|
348
|
+
onError?: (error: HelloPayError) => void;
|
|
349
|
+
/** ユーザーが Apple Pay シートを閉じた時。 */
|
|
350
|
+
onCancel?: () => void;
|
|
351
|
+
/** Apple Pay ボタンの色。既定 `black`。 */
|
|
352
|
+
buttonStyle?: 'black' | 'white' | 'white-outline';
|
|
353
|
+
/** Apple Pay ボタンの種別(`plain` / `buy` / `pay` 等)。既定 `plain`。 */
|
|
354
|
+
buttonType?: string;
|
|
355
|
+
className?: string;
|
|
356
|
+
style?: CSSProperties;
|
|
357
|
+
}
|
|
358
|
+
declare function ApplePayButton({ options, onToken, onError, onCancel, buttonStyle, buttonType, className, style, }: ApplePayButtonProps): react_jsx_runtime.JSX.Element | null;
|
|
359
|
+
declare namespace ApplePayButton {
|
|
360
|
+
var displayName: string;
|
|
361
|
+
}
|
|
169
362
|
|
|
170
363
|
/**
|
|
171
364
|
* React hooks.
|
|
@@ -178,4 +371,4 @@ declare const CardholderNameElement: {
|
|
|
178
371
|
declare function useHelloPay(): HelloPay | null;
|
|
179
372
|
declare function useElements(): Elements$1 | null;
|
|
180
373
|
|
|
181
|
-
export { CardCvcElement, CardExpiryElement, CardNumberElement, CardholderNameElement, type CreateTokenResult, type Element, type ElementChangeEvent, type ElementFocusEvent, type ElementOptions, type ElementProps, type ElementReadyEvent, type ElementStyle, type ElementType, Elements, type ElementsProps, type Elements$1 as ElementsSession, type HelloPay, type HelloPayError, type LoadHelloPayOptions, type TokenCard, type Unsubscribe, loadHelloPay, useElements, useHelloPay };
|
|
374
|
+
export { ApplePayButton, type ApplePayButtonProps, type ApplePayCancelError, type ApplePayCard, type ApplePayError, type ApplePayOptions, type ApplePayResult, CardCvcElement, CardExpiryElement, type CardNumberChangeEvent, CardNumberElement, CardPasswordElement, CardholderNameElement, type CreateTokenResult, type Element, type ElementChangeEvent, type ElementFocusEvent, type ElementOptions, type ElementProps, type ElementReadyEvent, type ElementStyle, type ElementType, Elements, type ElementsProps, type Elements$1 as ElementsSession, type HelloPay, type HelloPayError, type IssuerCountryResult, type LoadHelloPayOptions, type OtherElementChangeEvent, type TokenCard, type TokenProvider, type Unsubscribe, loadHelloPay, useElements, useHelloPay };
|
package/dist/index.js
CHANGED
|
@@ -135,7 +135,7 @@ function Elements({ hellopay: hellopayProp, children }) {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
// src/ElementComponents.tsx
|
|
138
|
-
import { useEffect as useEffect2, useRef } from "react";
|
|
138
|
+
import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
|
|
139
139
|
|
|
140
140
|
// src/hooks.ts
|
|
141
141
|
import { useContext } from "react";
|
|
@@ -148,6 +148,24 @@ function useElements() {
|
|
|
148
148
|
|
|
149
149
|
// src/ElementComponents.tsx
|
|
150
150
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
151
|
+
var PROBE_PX = 1e5;
|
|
152
|
+
function contentSizedAxes(container) {
|
|
153
|
+
if (container.getClientRects().length === 0) {
|
|
154
|
+
const computed = getComputedStyle(container);
|
|
155
|
+
const isAuto = (v) => v === "" || v === "auto";
|
|
156
|
+
return { width: isAuto(computed.width), height: isAuto(computed.height) };
|
|
157
|
+
}
|
|
158
|
+
const before = { width: container.offsetWidth, height: container.offsetHeight };
|
|
159
|
+
const probe = container.ownerDocument.createElement("div");
|
|
160
|
+
probe.style.cssText = `width: ${PROBE_PX}px; height: ${PROBE_PX}px;`;
|
|
161
|
+
container.appendChild(probe);
|
|
162
|
+
const grows = {
|
|
163
|
+
width: container.offsetWidth > before.width,
|
|
164
|
+
height: container.offsetHeight > before.height
|
|
165
|
+
};
|
|
166
|
+
probe.remove();
|
|
167
|
+
return grows;
|
|
168
|
+
}
|
|
151
169
|
function createElementComponent(type) {
|
|
152
170
|
function ElementComponent({
|
|
153
171
|
options,
|
|
@@ -160,12 +178,19 @@ function createElementComponent(type) {
|
|
|
160
178
|
}) {
|
|
161
179
|
const elements = useElements();
|
|
162
180
|
const containerRef = useRef(null);
|
|
181
|
+
const [fill, setFill] = useState2({ width: false, height: false });
|
|
163
182
|
const handlersRef = useRef({ onReady, onChange, onFocus, onBlur });
|
|
164
183
|
handlersRef.current = { onReady, onChange, onFocus, onBlur };
|
|
165
184
|
useEffect2(() => {
|
|
166
|
-
|
|
185
|
+
const container = containerRef.current;
|
|
186
|
+
if (!elements || !container) return;
|
|
187
|
+
const unset = contentSizedAxes(container);
|
|
188
|
+
setFill((prev) => ({
|
|
189
|
+
width: prev.width || unset.width,
|
|
190
|
+
height: prev.height || unset.height
|
|
191
|
+
}));
|
|
167
192
|
const element = elements.create(type, options);
|
|
168
|
-
element.mount(
|
|
193
|
+
element.mount(container);
|
|
169
194
|
const unsubs = [
|
|
170
195
|
element.on("ready", (e) => handlersRef.current.onReady?.(e)),
|
|
171
196
|
element.on("change", (e) => handlersRef.current.onChange?.(e)),
|
|
@@ -182,7 +207,11 @@ function createElementComponent(type) {
|
|
|
182
207
|
{
|
|
183
208
|
ref: containerRef,
|
|
184
209
|
className,
|
|
185
|
-
style: {
|
|
210
|
+
style: {
|
|
211
|
+
...fill.width && { width: "100%" },
|
|
212
|
+
...fill.height && { height: "100%" },
|
|
213
|
+
...style
|
|
214
|
+
}
|
|
186
215
|
}
|
|
187
216
|
);
|
|
188
217
|
}
|
|
@@ -194,10 +223,76 @@ var CardNumberElement = createElementComponent("cardNumber");
|
|
|
194
223
|
var CardExpiryElement = createElementComponent("cardExpiry");
|
|
195
224
|
var CardCvcElement = createElementComponent("cardCvc");
|
|
196
225
|
var CardholderNameElement = createElementComponent("cardholderName");
|
|
226
|
+
var CardPasswordElement = createElementComponent("cardPassword");
|
|
227
|
+
|
|
228
|
+
// src/ApplePayButton.tsx
|
|
229
|
+
import { useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
|
|
230
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
231
|
+
function ApplePayButton({
|
|
232
|
+
options,
|
|
233
|
+
onToken,
|
|
234
|
+
onError,
|
|
235
|
+
onCancel,
|
|
236
|
+
buttonStyle = "black",
|
|
237
|
+
buttonType = "plain",
|
|
238
|
+
className,
|
|
239
|
+
style
|
|
240
|
+
}) {
|
|
241
|
+
const hellopay = useHelloPay();
|
|
242
|
+
const buttonRef = useRef2(null);
|
|
243
|
+
const [supported, setSupported] = useState3(false);
|
|
244
|
+
useEffect3(() => {
|
|
245
|
+
setSupported(!!hellopay && hellopay.canMakeApplePayPayments());
|
|
246
|
+
}, [hellopay]);
|
|
247
|
+
useEffect3(() => {
|
|
248
|
+
const el = buttonRef.current;
|
|
249
|
+
if (!el) return;
|
|
250
|
+
el.style.setProperty("-webkit-appearance", "-apple-pay-button");
|
|
251
|
+
el.style.setProperty("-apple-pay-button-type", buttonType);
|
|
252
|
+
el.style.setProperty("-apple-pay-button-style", buttonStyle);
|
|
253
|
+
}, [supported, buttonType, buttonStyle]);
|
|
254
|
+
const latest = useRef2({ options, onToken, onError, onCancel, hellopay });
|
|
255
|
+
latest.current = { options, onToken, onError, onCancel, hellopay };
|
|
256
|
+
if (!supported) return null;
|
|
257
|
+
const handleClick = () => {
|
|
258
|
+
const current = latest.current;
|
|
259
|
+
if (!current.hellopay) return;
|
|
260
|
+
current.hellopay.applePay(current.options).then((result) => {
|
|
261
|
+
if (result.error) {
|
|
262
|
+
if (result.error.type === "cancel") {
|
|
263
|
+
current.onCancel?.();
|
|
264
|
+
} else {
|
|
265
|
+
current.onError?.(result.error);
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
current.onToken(result.token, result.card);
|
|
270
|
+
}).catch((err) => {
|
|
271
|
+
current.onError?.({
|
|
272
|
+
type: "config_error",
|
|
273
|
+
code: "apple_pay_error",
|
|
274
|
+
message: err instanceof Error ? err.message : "Apple Pay error"
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
return /* @__PURE__ */ jsx3(
|
|
279
|
+
"button",
|
|
280
|
+
{
|
|
281
|
+
ref: buttonRef,
|
|
282
|
+
type: "button",
|
|
283
|
+
onClick: handleClick,
|
|
284
|
+
className,
|
|
285
|
+
style: { width: "100%", height: "44px", cursor: "pointer", ...style }
|
|
286
|
+
}
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
ApplePayButton.displayName = "HelloPay(ApplePayButton)";
|
|
197
290
|
export {
|
|
291
|
+
ApplePayButton,
|
|
198
292
|
CardCvcElement,
|
|
199
293
|
CardExpiryElement,
|
|
200
294
|
CardNumberElement,
|
|
295
|
+
CardPasswordElement,
|
|
201
296
|
CardholderNameElement,
|
|
202
297
|
Elements,
|
|
203
298
|
loadHelloPay,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/loadHelloPay.ts","../src/Elements.tsx","../src/ElementComponents.tsx","../src/hooks.ts"],"sourcesContent":["/**\n * HelloPay SDK ローダー。\n *\n * HelloPay 管理下のドメイン(js.hello-pay.app 等)から SDK を動的ロードし、\n * `window.HelloPay` ファクトリを解決する。\n *\n * - 同一 sdkUrl への呼び出しは script tag を 1 回だけ挿入(dedup)\n * - 異なる sdkUrl は独立したロードを維持(E2E / ローカル切り替え対応)\n * - SSR 環境では Promise<null> を解決(加盟店側で null チェック)\n * - 一時的な失敗はキャッシュせず次回呼び出しで再試行可能\n */\n\nimport type { HelloPay, HelloPayFactory } from './types';\n\n// SDK 本体は test / live どちらの publishable key でも同一ドメイン\n// (js.hello-pay.app) から配信する。test / live mode の切り替えは SDK 内部で\n// publishable key の prefix から解決し、test mode では接続先を test 環境\n// (*.stg.hello-pay.app) に向ける(hellopay.js と同じ方式)。\nconst DEFAULT_SDK_URL = 'https://js.hello-pay.app/v1/hellopay-elements.js';\n\n// sdkUrl 単位で script ロード Promise をキャッシュ。\n// loadPromise を単一変数にするとすべての URL で共有されてしまい、\n// 異なる sdkUrl(E2E / ローカル開発)が誤って同一ロードを共有する。\nconst scriptPromiseCache = new Map<string, Promise<HelloPayFactory | null>>();\n\n// publicKey + locale + sdkUrl の組み合わせごとに `HelloPaySDK` インスタンスをキャッシュ。\n// 加盟店が誤って inline で loadHelloPay を呼び出した場合でも同一 SDK インスタンスが\n// 返るようにして、Elements セッションの BroadcastChannel session ID ミスマッチを防ぐ。\nconst sdkCache = new Map<string, Promise<HelloPay | null>>();\n\n/** テスト専用: module-scope のキャッシュをすべて初期状態に戻す。 */\nexport function __resetLoadPromiseForTest(): void {\n scriptPromiseCache.clear();\n sdkCache.clear();\n}\n\nfunction normalizeSdkUrl(url: string): string {\n if (typeof window === 'undefined') return url;\n\n try {\n return new URL(url, window.location.href).href;\n } catch {\n return url;\n }\n}\n\nfunction normalizeLocale(locale?: string): string {\n return locale || 'ja';\n}\n\nfunction loadScript(url: string): Promise<HelloPayFactory | null> {\n if (typeof window === 'undefined') return Promise.resolve(null);\n\n // URL を正規化してプロトコル省略形などを統一する。\n // テスト / SSR 環境では window.location が http URL でない場合があるため try-catch で保護。\n const normalizedUrl = normalizeSdkUrl(url);\n\n return new Promise((resolve, reject) => {\n // script は URL 単位で管理する。異なる URL の script を削除せず共存させることで、\n // E2E / ローカル開発で異なる sdkUrl が並列で呼ばれても Promise が宙吊りにならない。\n // 状態は dataset.hellopayState に \"loading\" | \"loaded\" | \"error\" で刻む。\n const managedScripts = Array.from(\n document.querySelectorAll<HTMLScriptElement>('script[data-hellopay-sdk]'),\n );\n\n const existing = managedScripts.find((script) => {\n const rawUrl = script.dataset.hellopayUrl || script.src;\n if (!rawUrl) return false;\n try {\n return new URL(rawUrl, window.location.href).href === normalizedUrl;\n } catch {\n return rawUrl === normalizedUrl;\n }\n });\n\n if (existing) {\n const state = existing.dataset.hellopayState;\n if (state === 'loaded') {\n resolve(window.HelloPay ?? null);\n return;\n }\n if (state === 'loading') {\n existing.addEventListener('load', () => resolve(window.HelloPay ?? null), {\n once: true,\n });\n existing.addEventListener(\n 'error',\n () => reject(new Error('HelloPay SDK failed to load')),\n { once: true },\n );\n return;\n }\n // state === \"error\" or undefined: 失敗済み or 未知の状態なので作り直す\n existing.remove();\n }\n\n const script = document.createElement('script');\n script.src = normalizedUrl;\n script.async = true;\n script.dataset.hellopaySdk = 'true';\n script.dataset.hellopayUrl = normalizedUrl; // URL 比較用\n script.dataset.hellopayState = 'loading';\n script.addEventListener(\n 'load',\n () => {\n if (!window.HelloPay) {\n script.dataset.hellopayState = 'error';\n reject(new Error('HelloPay SDK loaded but window.HelloPay is missing'));\n return;\n }\n script.dataset.hellopayState = 'loaded';\n resolve(window.HelloPay);\n },\n { once: true },\n );\n script.addEventListener(\n 'error',\n () => {\n script.dataset.hellopayState = 'error';\n reject(new Error('HelloPay SDK failed to load'));\n },\n { once: true },\n );\n document.head.appendChild(script);\n });\n}\n\nexport interface LoadHelloPayOptions {\n /** SDK 配信 URL(ローカル開発・E2E で差し替える用。省略時は js.hello-pay.app から配信し、test/live は SDK 内部で key prefix から解決) */\n sdkUrl?: string;\n locale?: string;\n}\n\n/**\n * HelloPay SDK をロードして初期化する。\n * モジュールスコープで 1 回だけ呼び、返り値の Promise を `<Elements>` に渡す。\n */\nexport function loadHelloPay(\n publicKey: string,\n options: LoadHelloPayOptions = {},\n): Promise<HelloPay | null> {\n const sdkUrl = normalizeSdkUrl(options.sdkUrl ?? DEFAULT_SDK_URL);\n const locale = normalizeLocale(options.locale);\n const cacheKey = `${publicKey}::${locale}::${sdkUrl}`;\n const cached = sdkCache.get(cacheKey);\n if (cached) return cached;\n\n let scriptPromise = scriptPromiseCache.get(sdkUrl);\n if (!scriptPromise) {\n scriptPromise = loadScript(sdkUrl).catch((err) => {\n // 一時的なロード失敗はキャッシュせず次回呼び出しで再試行可能にする\n scriptPromiseCache.delete(sdkUrl);\n throw err;\n });\n scriptPromiseCache.set(sdkUrl, scriptPromise);\n }\n\n const sdkPromise = scriptPromise\n .then((factory) => {\n if (!factory) return null; // SSR\n return factory(publicKey, { locale: options.locale });\n })\n .catch((err) => {\n // SDK インスタンス生成失敗は sdkCache から退避して次回呼び出しで再試行可能にする\n sdkCache.delete(cacheKey);\n throw err;\n });\n sdkCache.set(cacheKey, sdkPromise);\n return sdkPromise;\n}\n","/**\n * <Elements> Provider.\n *\n * loadHelloPay() の Promise を受け取り、SDK インスタンスと Elements セッションを\n * 子孫に Context 経由で供給する。\n */\n\nimport { createContext, useEffect, useState, type ReactNode } from 'react';\nimport type { HelloPay, Elements as ElementsSession } from './types';\n\nexport interface ElementsContextValue {\n hellopay: HelloPay | null;\n elements: ElementsSession | null;\n}\n\nconst EMPTY_CONTEXT: ElementsContextValue = {\n hellopay: null,\n elements: null,\n};\n\nexport const ElementsContext = createContext<ElementsContextValue>(EMPTY_CONTEXT);\n\nexport interface ElementsProps {\n /** loadHelloPay() の戻り値。モジュールスコープで 1 回だけ生成すること */\n hellopay: Promise<HelloPay | null> | HelloPay | null;\n children: ReactNode;\n}\n\nexport function Elements({ hellopay: hellopayProp, children }: ElementsProps) {\n const [ctx, setCtx] = useState<ElementsContextValue>(EMPTY_CONTEXT);\n\n useEffect(() => {\n let cancelled = false;\n setCtx(EMPTY_CONTEXT);\n\n Promise.resolve(hellopayProp)\n .then((sdk) => {\n if (cancelled) return;\n if (!sdk) {\n setCtx(EMPTY_CONTEXT);\n return;\n }\n setCtx({ hellopay: sdk, elements: sdk.elements() });\n })\n .catch(() => {\n // SDK ロード失敗時は null のまま(unhandled rejection を防ぐ)\n if (cancelled) return;\n setCtx(EMPTY_CONTEXT);\n });\n\n return () => {\n cancelled = true;\n };\n }, [hellopayProp]);\n\n return <ElementsContext.Provider value={ctx}>{children}</ElementsContext.Provider>;\n}\n","/**\n * Element コンポーネント。\n *\n * 各コンポーネントは 1 つの `<div>` コンテナを描画するだけで、\n * 実体(カード情報が入る iframe)はコア SDK が差し込む。\n *\n * React Strict Mode の二重 mount 対応:\n * - effect の cleanup で element.unmount() を必ず呼ぶ\n * - unmount 後は Elements._elements 配列から除外される(コア SDK 側で対応済)\n * - 結果として Strict Mode で effect が再実行されても古い iframe は残らない\n */\n\nimport { useEffect, useRef, type CSSProperties } from 'react';\nimport { useElements } from './hooks';\nimport type {\n ElementType,\n ElementOptions,\n ElementChangeEvent,\n ElementFocusEvent,\n ElementReadyEvent,\n} from './types';\n\nexport interface ElementProps {\n options?: ElementOptions;\n className?: string;\n style?: CSSProperties;\n onReady?: (e: ElementReadyEvent) => void;\n onChange?: (e: ElementChangeEvent) => void;\n onFocus?: (e: ElementFocusEvent) => void;\n onBlur?: (e: ElementFocusEvent) => void;\n}\n\n/**\n * 指定 type の Element を管理する React コンポーネントを生成する。\n */\nfunction createElementComponent(type: ElementType) {\n function ElementComponent({\n options,\n className,\n style,\n onReady,\n onChange,\n onFocus,\n onBlur,\n }: ElementProps) {\n const elements = useElements();\n const containerRef = useRef<HTMLDivElement | null>(null);\n\n // 最新のハンドラを ref で保持(effect を毎回 re-run させないため)\n const handlersRef = useRef({ onReady, onChange, onFocus, onBlur });\n handlersRef.current = { onReady, onChange, onFocus, onBlur };\n\n useEffect(() => {\n if (!elements || !containerRef.current) return;\n\n const element = elements.create(type, options);\n element.mount(containerRef.current);\n\n const unsubs = [\n element.on('ready', (e) => handlersRef.current.onReady?.(e)),\n element.on('change', (e) => handlersRef.current.onChange?.(e)),\n element.on('focus', (e) => handlersRef.current.onFocus?.(e)),\n element.on('blur', (e) => handlersRef.current.onBlur?.(e)),\n ];\n\n return () => {\n unsubs.forEach((u) => u());\n element.unmount();\n };\n // options はマウント時にしか反映しない方針(Stripe と同じ挙動)。\n // 動的に style を変えたい場合は後続 PR で element.update() を追加。\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [elements]);\n\n // 親コンテナ(加盟店が指定した `.element-container` 等)の高さを\n // iframe に伝播させるため、wrapper は親のサイズを塗りつぶす。\n // style prop で上書き可能。\n return (\n <div\n ref={containerRef}\n className={className}\n style={{ width: '100%', height: '100%', ...style }}\n />\n );\n }\n ElementComponent.displayName = `HelloPay(${type})`;\n // getElement(CardNumberElement) のようにコンポーネント自身を型指定子として使えるよう、\n // 静的プロパティに type を生やしておく\n ElementComponent.__elementType = type as ElementType;\n return ElementComponent;\n}\n\nexport const CardNumberElement = createElementComponent('cardNumber');\nexport const CardExpiryElement = createElementComponent('cardExpiry');\nexport const CardCvcElement = createElementComponent('cardCvc');\nexport const CardholderNameElement = createElementComponent('cardholderName');\n\nexport type ElementComponent = typeof CardNumberElement;\n","/**\n * React hooks.\n *\n * useHelloPay / useElements は <Elements> Provider の Context から\n * SDK インスタンスと Elements セッションを取り出す。\n * SDK ロード完了までは null を返すので、ハンドラ内で null チェックする。\n */\n\nimport { useContext } from 'react';\nimport { ElementsContext } from './Elements';\nimport type { HelloPay, Elements } from './types';\n\nexport function useHelloPay(): HelloPay | null {\n return useContext(ElementsContext).hellopay;\n}\n\nexport function useElements(): Elements | null {\n return useContext(ElementsContext).elements;\n}\n"],"mappings":"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","names":["script","useEffect","jsx","useEffect"]}
|
|
1
|
+
{"version":3,"sources":["../src/loadHelloPay.ts","../src/Elements.tsx","../src/ElementComponents.tsx","../src/hooks.ts","../src/ApplePayButton.tsx"],"sourcesContent":["/**\n * HelloPay SDK ローダー。\n *\n * HelloPay 管理下のドメイン(js.hello-pay.app 等)から SDK を動的ロードし、\n * `window.HelloPay` ファクトリを解決する。\n *\n * - 同一 sdkUrl への呼び出しは script tag を 1 回だけ挿入(dedup)\n * - 異なる sdkUrl は独立したロードを維持(E2E / ローカル切り替え対応)\n * - SSR 環境では Promise<null> を解決(加盟店側で null チェック)\n * - 一時的な失敗はキャッシュせず次回呼び出しで再試行可能\n */\n\nimport type { HelloPay, HelloPayFactory } from './types';\n\n// SDK 本体は test / live どちらの publishable key でも同一ドメイン\n// (js.hello-pay.app) から配信する。test / live mode の切り替えは SDK 内部で\n// publishable key の prefix から解決し、test mode では接続先を test 環境\n// (*.stg.hello-pay.app) に向ける(hellopay.js と同じ方式)。\nconst DEFAULT_SDK_URL = 'https://js.hello-pay.app/v1/hellopay-elements.js';\n\n// sdkUrl 単位で script ロード Promise をキャッシュ。\n// loadPromise を単一変数にするとすべての URL で共有されてしまい、\n// 異なる sdkUrl(E2E / ローカル開発)が誤って同一ロードを共有する。\nconst scriptPromiseCache = new Map<string, Promise<HelloPayFactory | null>>();\n\n// publicKey + locale + sdkUrl の組み合わせごとに `HelloPaySDK` インスタンスをキャッシュ。\n// 加盟店が誤って inline で loadHelloPay を呼び出した場合でも同一 SDK インスタンスが\n// 返るようにして、Elements セッションの BroadcastChannel session ID ミスマッチを防ぐ。\nconst sdkCache = new Map<string, Promise<HelloPay | null>>();\n\n/** テスト専用: module-scope のキャッシュをすべて初期状態に戻す。 */\nexport function __resetLoadPromiseForTest(): void {\n scriptPromiseCache.clear();\n sdkCache.clear();\n}\n\nfunction normalizeSdkUrl(url: string): string {\n if (typeof window === 'undefined') return url;\n\n try {\n return new URL(url, window.location.href).href;\n } catch {\n return url;\n }\n}\n\nfunction normalizeLocale(locale?: string): string {\n return locale || 'ja';\n}\n\nfunction loadScript(url: string): Promise<HelloPayFactory | null> {\n if (typeof window === 'undefined') return Promise.resolve(null);\n\n // URL を正規化してプロトコル省略形などを統一する。\n // テスト / SSR 環境では window.location が http URL でない場合があるため try-catch で保護。\n const normalizedUrl = normalizeSdkUrl(url);\n\n return new Promise((resolve, reject) => {\n // script は URL 単位で管理する。異なる URL の script を削除せず共存させることで、\n // E2E / ローカル開発で異なる sdkUrl が並列で呼ばれても Promise が宙吊りにならない。\n // 状態は dataset.hellopayState に \"loading\" | \"loaded\" | \"error\" で刻む。\n const managedScripts = Array.from(\n document.querySelectorAll<HTMLScriptElement>('script[data-hellopay-sdk]'),\n );\n\n const existing = managedScripts.find((script) => {\n const rawUrl = script.dataset.hellopayUrl || script.src;\n if (!rawUrl) return false;\n try {\n return new URL(rawUrl, window.location.href).href === normalizedUrl;\n } catch {\n return rawUrl === normalizedUrl;\n }\n });\n\n if (existing) {\n const state = existing.dataset.hellopayState;\n if (state === 'loaded') {\n resolve(window.HelloPay ?? null);\n return;\n }\n if (state === 'loading') {\n existing.addEventListener('load', () => resolve(window.HelloPay ?? null), {\n once: true,\n });\n existing.addEventListener(\n 'error',\n () => reject(new Error('HelloPay SDK failed to load')),\n { once: true },\n );\n return;\n }\n // state === \"error\" or undefined: 失敗済み or 未知の状態なので作り直す\n existing.remove();\n }\n\n const script = document.createElement('script');\n script.src = normalizedUrl;\n script.async = true;\n script.dataset.hellopaySdk = 'true';\n script.dataset.hellopayUrl = normalizedUrl; // URL 比較用\n script.dataset.hellopayState = 'loading';\n script.addEventListener(\n 'load',\n () => {\n if (!window.HelloPay) {\n script.dataset.hellopayState = 'error';\n reject(new Error('HelloPay SDK loaded but window.HelloPay is missing'));\n return;\n }\n script.dataset.hellopayState = 'loaded';\n resolve(window.HelloPay);\n },\n { once: true },\n );\n script.addEventListener(\n 'error',\n () => {\n script.dataset.hellopayState = 'error';\n reject(new Error('HelloPay SDK failed to load'));\n },\n { once: true },\n );\n document.head.appendChild(script);\n });\n}\n\nexport interface LoadHelloPayOptions {\n /** SDK 配信 URL(ローカル開発・E2E で差し替える用。省略時は js.hello-pay.app から配信し、test/live は SDK 内部で key prefix から解決) */\n sdkUrl?: string;\n locale?: string;\n}\n\n/**\n * HelloPay SDK をロードして初期化する。\n * モジュールスコープで 1 回だけ呼び、返り値の Promise を `<Elements>` に渡す。\n */\nexport function loadHelloPay(\n publicKey: string,\n options: LoadHelloPayOptions = {},\n): Promise<HelloPay | null> {\n const sdkUrl = normalizeSdkUrl(options.sdkUrl ?? DEFAULT_SDK_URL);\n const locale = normalizeLocale(options.locale);\n const cacheKey = `${publicKey}::${locale}::${sdkUrl}`;\n const cached = sdkCache.get(cacheKey);\n if (cached) return cached;\n\n let scriptPromise = scriptPromiseCache.get(sdkUrl);\n if (!scriptPromise) {\n scriptPromise = loadScript(sdkUrl).catch((err) => {\n // 一時的なロード失敗はキャッシュせず次回呼び出しで再試行可能にする\n scriptPromiseCache.delete(sdkUrl);\n throw err;\n });\n scriptPromiseCache.set(sdkUrl, scriptPromise);\n }\n\n const sdkPromise = scriptPromise\n .then((factory) => {\n if (!factory) return null; // SSR\n return factory(publicKey, { locale: options.locale });\n })\n .catch((err) => {\n // SDK インスタンス生成失敗は sdkCache から退避して次回呼び出しで再試行可能にする\n sdkCache.delete(cacheKey);\n throw err;\n });\n sdkCache.set(cacheKey, sdkPromise);\n return sdkPromise;\n}\n","/**\n * <Elements> Provider.\n *\n * loadHelloPay() の Promise を受け取り、SDK インスタンスと Elements セッションを\n * 子孫に Context 経由で供給する。\n */\n\nimport { createContext, useEffect, useState, type ReactNode } from 'react';\nimport type { HelloPay, Elements as ElementsSession } from './types';\n\nexport interface ElementsContextValue {\n hellopay: HelloPay | null;\n elements: ElementsSession | null;\n}\n\nconst EMPTY_CONTEXT: ElementsContextValue = {\n hellopay: null,\n elements: null,\n};\n\nexport const ElementsContext = createContext<ElementsContextValue>(EMPTY_CONTEXT);\n\nexport interface ElementsProps {\n /** loadHelloPay() の戻り値。モジュールスコープで 1 回だけ生成すること */\n hellopay: Promise<HelloPay | null> | HelloPay | null;\n children: ReactNode;\n}\n\nexport function Elements({ hellopay: hellopayProp, children }: ElementsProps) {\n const [ctx, setCtx] = useState<ElementsContextValue>(EMPTY_CONTEXT);\n\n useEffect(() => {\n let cancelled = false;\n setCtx(EMPTY_CONTEXT);\n\n Promise.resolve(hellopayProp)\n .then((sdk) => {\n if (cancelled) return;\n if (!sdk) {\n setCtx(EMPTY_CONTEXT);\n return;\n }\n setCtx({ hellopay: sdk, elements: sdk.elements() });\n })\n .catch(() => {\n // SDK ロード失敗時は null のまま(unhandled rejection を防ぐ)\n if (cancelled) return;\n setCtx(EMPTY_CONTEXT);\n });\n\n return () => {\n cancelled = true;\n };\n }, [hellopayProp]);\n\n return <ElementsContext.Provider value={ctx}>{children}</ElementsContext.Provider>;\n}\n","/**\n * Element コンポーネント。\n *\n * 各コンポーネントは 1 つの `<div>` コンテナを描画するだけで、\n * 実体(カード情報が入る iframe)はコア SDK が差し込む。\n *\n * React Strict Mode の二重 mount 対応:\n * - effect の cleanup で element.unmount() を必ず呼ぶ\n * - unmount 後は Elements._elements 配列から除外される(コア SDK 側で対応済)\n * - 結果として Strict Mode で effect が再実行されても古い iframe は残らない\n */\n\nimport { useEffect, useRef, useState, type CSSProperties } from 'react';\nimport { useElements } from './hooks';\nimport type {\n ElementType,\n ElementOptions,\n ElementChangeEvent,\n ElementFocusEvent,\n ElementReadyEvent,\n} from './types';\n\nexport interface ElementProps {\n options?: ElementOptions;\n className?: string;\n style?: CSSProperties;\n onReady?: (e: ElementReadyEvent) => void;\n onChange?: (e: ElementChangeEvent) => void;\n onFocus?: (e: ElementFocusEvent) => void;\n onBlur?: (e: ElementFocusEvent) => void;\n}\n\nconst PROBE_PX = 100000;\n\n/**\n * コンテナの幅・高さのうち、中身に追従する(加盟店が寸法を決めていない)軸を返す。\n */\nfunction contentSizedAxes(container: HTMLElement) {\n // display: none の祖先の中ではレイアウトが無いので、computed value で見る。\n if (container.getClientRects().length === 0) {\n const computed = getComputedStyle(container);\n const isAuto = (v: string) => v === '' || v === 'auto';\n return { width: isAuto(computed.width), height: isAuto(computed.height) };\n }\n // computed の height で判定すると、border-box の padding・border や min-height で\n // 0 にならず見誤るので、大きな子を一時的に入れて寸法が伸びるかで確かめる。\n // 祖先の transform(モーダルの scale のアニメーションなど)に左右されないよう、\n // getBoundingClientRect ではなくレイアウト上の寸法の offset* で比べる。\n const before = { width: container.offsetWidth, height: container.offsetHeight };\n const probe = container.ownerDocument.createElement('div');\n probe.style.cssText = `width: ${PROBE_PX}px; height: ${PROBE_PX}px;`;\n container.appendChild(probe);\n const grows = {\n width: container.offsetWidth > before.width,\n height: container.offsetHeight > before.height,\n };\n probe.remove();\n return grows;\n}\n\n/**\n * 指定 type の Element を管理する React コンポーネントを生成する。\n */\nfunction createElementComponent(type: ElementType) {\n function ElementComponent({\n options,\n className,\n style,\n onReady,\n onChange,\n onFocus,\n onBlur,\n }: ElementProps) {\n const elements = useElements();\n const containerRef = useRef<HTMLDivElement | null>(null);\n const [fill, setFill] = useState({ width: false, height: false });\n\n // 最新のハンドラを ref で保持(effect を毎回 re-run させないため)\n const handlersRef = useRef({ onReady, onChange, onFocus, onBlur });\n handlersRef.current = { onReady, onChange, onFocus, onBlur };\n\n useEffect(() => {\n const container = containerRef.current;\n if (!elements || !container) return;\n\n // 100% を当てた後に測り直すと親の寸法に追従して見えるので、立てた軸は戻さない。\n const unset = contentSizedAxes(container);\n setFill((prev) => ({\n width: prev.width || unset.width,\n height: prev.height || unset.height,\n }));\n\n const element = elements.create(type, options);\n element.mount(container);\n\n const unsubs = [\n element.on('ready', (e) => handlersRef.current.onReady?.(e)),\n element.on('change', (e) => handlersRef.current.onChange?.(e)),\n element.on('focus', (e) => handlersRef.current.onFocus?.(e)),\n element.on('blur', (e) => handlersRef.current.onBlur?.(e)),\n ];\n\n return () => {\n unsubs.forEach((u) => u());\n element.unmount();\n };\n // options はマウント時にしか反映しない方針(Stripe と同じ挙動)。\n // 動的に style を変えたい場合は後続 PR で element.update() を追加。\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [elements]);\n\n // インラインの既定値は className の寸法に勝つので、加盟店が決めていない軸だけに付ける。\n // 判定は mount 時の 1 回だけなので、後から寸法を変えるなら style で渡す。\n return (\n <div\n ref={containerRef}\n className={className}\n style={{\n ...(fill.width && { width: '100%' }),\n ...(fill.height && { height: '100%' }),\n ...style,\n }}\n />\n );\n }\n ElementComponent.displayName = `HelloPay(${type})`;\n // getElement(CardNumberElement) のようにコンポーネント自身を型指定子として使えるよう、\n // 静的プロパティに type を生やしておく\n ElementComponent.__elementType = type as ElementType;\n return ElementComponent;\n}\n\nexport const CardNumberElement = createElementComponent('cardNumber');\nexport const CardExpiryElement = createElementComponent('cardExpiry');\nexport const CardCvcElement = createElementComponent('cardCvc');\nexport const CardholderNameElement = createElementComponent('cardholderName');\n// 韓国発行カードの Toss 登録に必要な暗証番号の先頭 2 桁。表示・非表示は\n// 発行国の判定結果に応じて呼び出し側が mount / unmount で制御する。\nexport const CardPasswordElement = createElementComponent('cardPassword');\n\nexport type ElementComponent = typeof CardNumberElement;\n","/**\n * React hooks.\n *\n * useHelloPay / useElements は <Elements> Provider の Context から\n * SDK インスタンスと Elements セッションを取り出す。\n * SDK ロード完了までは null を返すので、ハンドラ内で null チェックする。\n */\n\nimport { useContext } from 'react';\nimport { ElementsContext } from './Elements';\nimport type { HelloPay, Elements } from './types';\n\nexport function useHelloPay(): HelloPay | null {\n return useContext(ElementsContext).hellopay;\n}\n\nexport function useElements(): Elements | null {\n return useContext(ElementsContext).elements;\n}\n","/**\n * Apple Pay ボタン。\n *\n * `useHelloPay()` から SDK を取り、対応端末(Safari + Apple Pay 利用可能)でのみ\n * Apple 公式スタイルのボタンを描画する。非対応端末では何も描画しない。\n *\n * click で `hellopay.applePay()` を同期呼び出しし(`new ApplePaySession` の\n * user gesture 要件のため)、結果を `onToken` / `onError` / `onCancel` に振り分ける。\n * SDK は Apple Pay トークンを返すだけで課金はしないので、加盟店は `onToken` で\n * 受けたトークンを自社バックエンドから決済 API に送って課金する。\n */\n\nimport { useEffect, useRef, useState, type CSSProperties } from 'react';\nimport { useHelloPay } from './hooks';\nimport type { ApplePayCard, ApplePayOptions, HelloPayError } from './types';\n\nexport interface ApplePayButtonProps {\n options: ApplePayOptions;\n /** トークン取得成功時。トークンを自社バックエンド経由で決済 API に送る。 */\n onToken: (token: string, card: ApplePayCard) => void;\n /** validation / network エラー時。 */\n onError?: (error: HelloPayError) => void;\n /** ユーザーが Apple Pay シートを閉じた時。 */\n onCancel?: () => void;\n /** Apple Pay ボタンの色。既定 `black`。 */\n buttonStyle?: 'black' | 'white' | 'white-outline';\n /** Apple Pay ボタンの種別(`plain` / `buy` / `pay` 等)。既定 `plain`。 */\n buttonType?: string;\n className?: string;\n style?: CSSProperties;\n}\n\nexport function ApplePayButton({\n options,\n onToken,\n onError,\n onCancel,\n buttonStyle = 'black',\n buttonType = 'plain',\n className,\n style,\n}: ApplePayButtonProps) {\n const hellopay = useHelloPay();\n const buttonRef = useRef<HTMLButtonElement | null>(null);\n const [supported, setSupported] = useState(false);\n\n // canMakeApplePayPayments は ApplePaySession 依存で client-only のため effect で判定。\n useEffect(() => {\n setSupported(!!hellopay && hellopay.canMakeApplePayPayments());\n }, [hellopay]);\n\n // Apple Pay ボタンの見た目はベンダー CSS プロパティで指定する。React の style 型に\n // 無いため ref 経由で setProperty する。\n useEffect(() => {\n const el = buttonRef.current;\n if (!el) return;\n el.style.setProperty('-webkit-appearance', '-apple-pay-button');\n el.style.setProperty('-apple-pay-button-type', buttonType);\n el.style.setProperty('-apple-pay-button-style', buttonStyle);\n }, [supported, buttonType, buttonStyle]);\n\n // 最新のハンドラ / options を ref に保持し、click ハンドラを毎回作り直さない。\n const latest = useRef({ options, onToken, onError, onCancel, hellopay });\n latest.current = { options, onToken, onError, onCancel, hellopay };\n\n if (!supported) return null;\n\n const handleClick = () => {\n const current = latest.current;\n if (!current.hellopay) return;\n // user gesture 要件のため click ハンドラから同期呼び出しする。\n current.hellopay\n .applePay(current.options)\n .then((result) => {\n if (result.error) {\n if (result.error.type === 'cancel') {\n current.onCancel?.();\n } else {\n current.onError?.(result.error);\n }\n return;\n }\n current.onToken(result.token, result.card);\n })\n .catch((err: unknown) => {\n current.onError?.({\n type: 'config_error',\n code: 'apple_pay_error',\n message: err instanceof Error ? err.message : 'Apple Pay error',\n });\n });\n };\n\n return (\n <button\n ref={buttonRef}\n type=\"button\"\n onClick={handleClick}\n className={className}\n style={{ width: '100%', height: '44px', cursor: 'pointer', ...style }}\n />\n );\n}\n\nApplePayButton.displayName = 'HelloPay(ApplePayButton)';\n"],"mappings":"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","names":["script","useEffect","useState","jsx","useState","useEffect","useEffect","useRef","useState","jsx","useRef","useState","useEffect"]}
|