{"_id":"@clink-ai/clink-js","_rev":"4-33661459e982c33f43723494163bd0c3","name":"@clink-ai/clink-js","dist-tags":{"latest":"1.0.2"},"versions":{"1.0.0":{"name":"@clink-ai/clink-js","version":"1.0.0","_id":"@clink-ai/clink-js@1.0.0","maintainers":[{"name":"clink-official","email":"patrick@clinkbill.com"}],"dist":{"shasum":"9a948526679ddfbd0513e0fd968a22c84ef927e3","tarball":"https://registry.npmjs.org/@clink-ai/clink-js/-/clink-js-1.0.0.tgz","fileCount":11,"integrity":"sha512-d8CV9YMb5XLo1MCprap/FdTXZ/4jIhFOfQOQ5KRSbc1T78kB2Slzqi6UJhTwuuC5Oygi3blb/vcJKJIDk7JOgg==","signatures":[{"sig":"MEUCIElzrHRVjc7T5PLK1VQlha3fmP7+AOOM95mYc/i/7+FfAiEA7NYUhTcoqr7dOLipgk+F1RlYk1dznzYDR2T7FLoWxsQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":49300},"main":"./dist/index.umd.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.umd.js"}},"gitHead":"ca3c501f22748841200815c5d2ad09eb093951b0","scripts":{"test":"vitest run --config vitest.config.ts","build":"vite build --config vite.config.ts && tsc -p tsconfig.json"},"_npmUser":{"name":"clink-official","email":"patrick@clinkbill.com"},"deprecated":"Depreacted env viriables, use 1.0.1","_npmVersion":"11.5.1","description":"Clink hosted checkout JS SDK (V1)","directories":{},"sideEffects":false,"_nodeVersion":"24.5.0","_hasShrinkwrap":false,"devDependencies":{"vite":"7","vitest":"^3.2.4","typescript":"~5.7.2"},"_npmOperationalInternal":{"tmp":"tmp/clink-js_1.0.0_1774510910489_0.47852192323148857","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@clink-ai/clink-js","version":"1.0.1","_id":"@clink-ai/clink-js@1.0.1","maintainers":[{"name":"clink-official","email":"patrick@clinkbill.com"}],"dist":{"shasum":"c25ebb5ef39e94d00b26b228c73797e7acbaa067","tarball":"https://registry.npmjs.org/@clink-ai/clink-js/-/clink-js-1.0.1.tgz","fileCount":11,"integrity":"sha512-dCgPBY/SKiq4Ek0fxTYp1kX556JaqVxXDprIOG1+7weNEwjQtmOa73dQZKo7AmlpQsVbtpBt4kwMt2Ux3eyN7w==","signatures":[{"sig":"MEQCID6dXsh9vBLtlAuiXiR4eWw8WpiHBrQ4BFabwXvzcaD6AiA+n//xhy8/ghaHbmgvNgOQEHcJbpGr+9jpdGbskPXEgA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":49515},"main":"./dist/index.umd.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.umd.js"}},"gitHead":"e5238ce87f1fc26e55d647c41aada59efd3c015f","scripts":{"test":"vitest run --config vitest.config.ts","build":"vite build --config vite.config.ts && tsc -p tsconfig.json"},"_npmUser":{"name":"clink-official","email":"patrick@clinkbill.com"},"_npmVersion":"11.5.1","description":"Clink hosted checkout JS SDK (V1)","directories":{},"sideEffects":false,"_nodeVersion":"24.5.0","_hasShrinkwrap":false,"devDependencies":{"vite":"7","vitest":"^3.2.4","typescript":"~5.7.2"},"_npmOperationalInternal":{"tmp":"tmp/clink-js_1.0.1_1775132328839_0.2750245924903685","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@clink-ai/clink-js","version":"1.0.2","description":"Clink hosted checkout JS SDK (V1)","type":"module","main":"./dist/index.umd.js","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.umd.js"}},"sideEffects":false,"scripts":{"build":"vite build --config vite.config.ts && tsc -p tsconfig.json","test":"vitest run --config vitest.config.ts"},"devDependencies":{"typescript":"~5.7.2","vite":"7","vitest":"^3.2.4"},"_id":"@clink-ai/clink-js@1.0.2","gitHead":"3a6d32d475853b77437b997a0782be3c73bc82bf","_nodeVersion":"24.5.0","_npmVersion":"11.5.1","dist":{"integrity":"sha512-c7iXkHisprl2V3a+/V6JXKAweeC8OKKylWdF4h8mSKsZAIgpfENDrWGnXelwsOTI+MD/fSiJHRdx90zYOHXLKA==","shasum":"6e394aafbfcb19e87f8e6ea6361e286a0955a2dd","tarball":"https://registry.npmjs.org/@clink-ai/clink-js/-/clink-js-1.0.2.tgz","fileCount":11,"unpackedSize":49844,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDCwJrmIS0u0TbUppdFDwjVd6qS2Mpdp1IQ1cHhmTg/EwIgSTPcGHqoDnrx4VZVNHmDgkk+bXgiDexBTUx6QVNb5TM="}]},"_npmUser":{"name":"clink-official","email":"patrick@clinkbill.com"},"directories":{},"maintainers":[{"name":"clink-official","email":"patrick@clinkbill.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/clink-js_1.0.2_1775134155758_0.6879978183277213"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-26T07:41:50.369Z","modified":"2026-04-02T12:49:16.043Z","1.0.0":"2026-03-26T07:41:50.662Z","1.0.1":"2026-04-02T12:18:48.978Z","1.0.2":"2026-04-02T12:49:15.916Z"},"description":"Clink hosted checkout JS SDK (V1)","maintainers":[{"name":"clink-official","email":"patrick@clinkbill.com"}],"readme":"# Clink Embedded Checkout SDK\n\nClink Checkout SDK:\n- `redirectToCheckout`: hosted checkout redirect\n- `initEmbeddedCheckout`: embedded checkout\n\n## Installation\n\n```bash\n# Once published\nnpm install @clink/js\n# or pnpm add @clink/js\n\n# Local development inside this repository\nnpm --prefix packages/clink-js run test\nnpm --prefix packages/clink-js run build\n```\n\n## API\n\n### `loadClink(publicKey, options?)`\n\n```ts\nimport { loadClink } from '@clink/js';\n\nconst clink = await loadClink('pk_uat_xxx', {\n  checkoutEnvironment: 'sandbox',\n  locale: 'zh-CN',\n});\n```\n\n- `publicKey` must match `pk_test_*`, `pk_uat_*`, or `pk_prod_*`\n- Prefer `checkoutEnvironment` so the SDK can resolve the correct Clink bootstrap endpoint automatically, or pass `checkoutBaseUrl` directly\n- `checkoutEnvironment` is optional: `sandbox` / `production`\n- `checkoutBaseUrl` is optional: explicitly set the checkout base URL, for example `https://checkout.clinkbill.com`\n\n### `clink.redirectToCheckout(params)` (hosted checkout redirect)\n\n```ts\nawait clink.redirectToCheckout({\n  sessionParam: 'sess_123#token',\n  replace: false,\n});\n```\n\nBehavior:\n- At least one of `sessionParam` or `sessionId` is required\n- If both are provided, `sessionParam` takes precedence\n- Redirect URL format: `{checkoutBaseUrl}/pay/{encodeURIComponent(sessionParam)}`\n- `replace: true` uses `location.replace`; default is `location.assign`\n\n### `clink.initEmbeddedCheckout(options)` (embedded checkout)\n\n```ts\nimport { loadClink } from '@clink/js';\n\nconst clink = await loadClink('pk_uat_xxx');\n// Prefer passing checkoutEnvironment or checkoutBaseUrl explicitly\n// const clink = await loadClink('pk_uat_xxx', { checkoutEnvironment: 'sandbox' });\n\nconst embedded = await clink.initEmbeddedCheckout({\n  fetchSession: async () => {\n    const resp = await fetch('/api/checkout/session', { method: 'POST' });\n    const data = await resp.json();\n    return {\n      checkoutUrl: data.checkoutUrl as string,\n      sessionId: data.sessionId as string,\n      orderId: data.orderId as string,\n    };\n  },\n  pollStatus: async ({ orderId }) => {\n    if (!orderId) return null;\n\n    const resp = await fetch(`/api/topup/status?order_id=${orderId}`);\n    const data = await resp.json();\n\n    if (data.credited) return 'success';\n    if (data.status === 'failed') return 'error';\n    if (data.status === 'refunded') return 'cancelled';\n    return 'pending';\n  },\n  onEvent(event) {\n    console.log('[embedded event]', event.type, event.payload);\n  },\n  // default true\n  autoDestroyOnComplete: true,\n});\n\nembedded.mount('#checkout');\n```\n\nReact integration example:\n\n```tsx\nimport { useEffect, useRef, useState } from 'react';\nimport {\n  CLINK_ERROR_CODES,\n  ClinkError,\n  loadClink,\n  type EmbeddedCheckout,\n} from '@clink/js';\n\ninterface CheckoutPageProps {\n  publicKey: string;\n}\n\nexport function CheckoutPage({ publicKey }: CheckoutPageProps) {\n  const containerRef = useRef<HTMLDivElement | null>(null);\n  const embeddedRef = useRef<EmbeddedCheckout | null>(null);\n  const [loading, setLoading] = useState(true);\n  const [error, setError] = useState<string | null>(null);\n\n  useEffect(() => {\n    let cancelled = false;\n\n    async function setup() {\n      try {\n        const clink = await loadClink(publicKey, {\n          checkoutEnvironment: 'production',\n          locale: 'en-US',\n        });\n\n        const embedded = await clink.initEmbeddedCheckout({\n          fetchSession: async () => {\n            const resp = await fetch('/api/checkout/session', {\n              method: 'POST',\n              headers: { 'Content-Type': 'application/json' },\n            });\n\n            if (!resp.ok) {\n              throw new Error('failed to create checkout session');\n            }\n\n            const data = (await resp.json()) as {\n              checkoutUrl: string;\n              sessionId: string;\n              orderId?: string;\n            };\n            return data;\n          },\n          pollStatus: async ({ orderId }) => {\n            if (!orderId) {\n              return null;\n            }\n\n            const resp = await fetch(`/api/topup/status?order_id=${orderId}`);\n            if (!resp.ok) {\n              return null;\n            }\n\n            const data = (await resp.json()) as {\n              credited?: boolean;\n              status?: string;\n            };\n\n            if (data.credited || data.status === 'paid') {\n              return 'success';\n            }\n            if (data.status === 'failed') {\n              return 'error';\n            }\n            if (data.status === 'refunded') {\n              return 'cancelled';\n            }\n            return 'pending';\n          },\n          onEvent(event) {\n            if (event.type === 'hosted_return') {\n              window.location.assign('/payment/result');\n              return;\n            }\n\n            if (event.type === 'complete' && event.payload?.state === 'success') {\n              window.location.assign('/payment/success');\n              return;\n            }\n\n            if (event.type === 'error') {\n              setError('Payment failed. Please try again.');\n            }\n          },\n        });\n\n        if (cancelled || !containerRef.current) {\n          embedded.destroy();\n          return;\n        }\n\n        embedded.mount(containerRef.current);\n        embeddedRef.current = embedded;\n        setLoading(false);\n      } catch (err) {\n        if (cancelled) {\n          return;\n        }\n\n        if (\n          err instanceof ClinkError &&\n          err.code === CLINK_ERROR_CODES.SESSION_ID_FETCH_FAILED\n        ) {\n          setError('Unable to create checkout session.');\n        } else {\n          setError('Unable to load checkout.');\n        }\n        setLoading(false);\n      }\n    }\n\n    void setup();\n\n    return () => {\n      cancelled = true;\n      embeddedRef.current?.destroy();\n      embeddedRef.current = null;\n    };\n  }, [publicKey]);\n\n  return (\n    <div>\n      {loading ? <div>Loading checkout...</div> : null}\n      {error ? <div role=\"alert\">{error}</div> : null}\n      <div ref={containerRef} id=\"checkout\" />\n    </div>\n  );\n}\n```\n\nReact integration recommendations:\n- Initialize `loadClink()` and `initEmbeddedCheckout()` once inside `useEffect`\n- Store the `EmbeddedCheckout` instance in a `ref` and call `destroy()` on unmount\n- Listen to `complete` for the business final state; listen to `hosted_return` for UI cleanup after merchant-defined success or cancel return pages\n- `fetchSession()` must return `{ checkoutUrl, sessionId, orderId? }`, and `checkoutUrl` should be the `url` returned by your server-side `createCheckoutSession()`\n- By default the SDK destroys the iframe automatically after a successful `complete`; pass `autoDestroyOnComplete: false` if you need to keep it mounted\n- `fetchSession()` should call only your own backend; never expose secret keys in the frontend\n\n`options`:\n- `fetchSession`: calls your backend and must return `{ checkoutUrl, sessionId, orderId? }`\n- `onEvent` (optional): unified event callback\n- `autoResize` (optional, default `true`): automatically update iframe height from `resize` events\n- `autoDestroyOnComplete` (optional, default `true`): destroy the iframe after completion so the host page can take over the success state\n- `pollStatus` (optional): fallback polling for merchant-confirmed payments; the SDK polls on `pollIntervalMs` and emits `complete` automatically on terminal states\n- `pollIntervalMs` (optional, default `2000`)\n\nInstance API:\n- `mount(container: string | HTMLElement)`\n- `unmount()`\n- `destroy()`\n- `on(type, handler)` (returns an unsubscribe function)\n- `getState() -> { mounted, destroyed }`\n\nEvent types:\n- `ready`\n- `resize`\n- `state_change`\n- `complete`\n- `hosted_return`\n- `error`\n\nRecommended mental model:\n- `complete`: payment terminal-state event. It comes either from the checkout page itself or from terminal-state confirmation via `pollStatus`. Treat this as the source of truth for payment status.\n- `hosted_return`: UI cleanup event after the merchant's custom `successUrl` / `cancelUrl` return page. Use it to close the iframe, return to the host page, or show your own result screen.\n- `error`: SDK or polling error. This does not necessarily mean the payment itself failed.\n\n### Embedded checkout error handling example\n\n```ts\nimport { ClinkError, CLINK_ERROR_CODES } from '@clink/js';\n\ntry {\n  const embedded = await clink.initEmbeddedCheckout({\n    fetchSession: async () => ({ checkoutUrl: '', sessionId: 'sess_xxx' }),\n  });\n  embedded.mount('#checkout');\n} catch (error) {\n  if (error instanceof ClinkError) {\n    if (error.code === CLINK_ERROR_CODES.INVALID_SESSION_ID) {\n      console.error('Invalid checkoutUrl or sessionId');\n    }\n  }\n}\n```\n\n### Bootstrap environment control\n\nThe SDK supports fixing the remote bootstrap environment via environment variables:\n\n- `CLINK_ENV=sandbox` -> `https://uat-api.clinkbill.com/api/sdk/bootstrap`\n- `CLINK_ENV=production` -> `https://api.clinkbill.com/api/sdk/bootstrap`\n\n`checkoutEnvironment` should match `CLINK_ENV`, using `sandbox` / `production`.\n\nPriority order (high -> low):\n\n1. `loadClink(..., { checkoutBaseUrl })`\n2. `loadClink(..., { checkoutEnvironment })`\n3. `CLINK_ENV`\n\nRemote bootstrap request format:\n\n```http\nPOST /api/sdk/bootstrap\nX-API-Key: pk_uat_xxx / pk_prod_xxx\nX-Timestamp: <unix_ms>\nAccept-Language: zh-CN, en-US;q=0.9\nContent-Type: application/json\n```\n\nRequest body:\n\n```json\n{\n  \"origin\": \"https://merchant.example.com\",\n  \"sdkVersion\": \"1.0.0\",\n  \"locale\": \"zh_CN\"\n}\n```\n\nSuccessful response:\n\n```json\n{\n  \"code\": 200,\n  \"msg\": \"Success\",\n  \"data\": {\n    \"checkoutBaseUrl\": \"https://checkout.clinkbill.com\",\n    \"merchantId\": \"mcht_xxx\",\n    \"merchantName\": \"Demo Merchant\",\n    \"environment\": \"production\",\n    \"mode\": \"production\",\n    \"features\": {\n      \"embeddedCheckout\": true\n    },\n    \"allowedParentOrigins\": [\"https://merchant.example.com\"]\n  }\n}\n```\n\n## Error handling\n\nThe SDK throws `ClinkError` with a `code` field:\n\n```ts\nimport { ClinkError, CLINK_ERROR_CODES, loadClink } from '@clink/js';\n\ntry {\n  const clink = await loadClink('pk_prod_xxx');\n  await clink.redirectToCheckout({ sessionId: 'sess_001' });\n} catch (error) {\n  if (error instanceof ClinkError) {\n    if (error.code === CLINK_ERROR_CODES.INVALID_PUBLIC_KEY) {\n      console.error('Invalid public key format');\n    }\n  }\n}\n```\n\nCommon error codes:\n- `INVALID_PUBLIC_KEY`\n- `INVALID_CHECKOUT_ENV`\n- `BOOTSTRAP_REQUEST_FAILED`\n- `INVALID_BOOTSTRAP_RESPONSE`\n- `INVALID_REDIRECT_PARAMS`\n- `INVALID_EMBEDDED_OPTIONS`\n- `INVALID_SESSION_ID`\n- `SESSION_ID_FETCH_FAILED`\n- `EMBEDDED_CHECKOUT_DISABLED`\n- `CONTAINER_NOT_FOUND`\n- `NOT_IN_BROWSER`\n\n## Security boundaries\n\n- The SDK does not handle sensitive payment data such as card numbers or CVV\n- The SDK does not contain any secret-key logic\n- The SDK uses only `publicKey` for bootstrap requests\n- For embedded checkout, the recommended flow is for the merchant backend to create the checkout session and return `checkoutUrl`, `sessionId`, and related metadata to the frontend\n- `complete` only means checkout or backend-confirmed terminal state; use `hosted_return` for merchant return-page UI cleanup instead of payment confirmation\n\n## Integration guide\n\n- Use `loadClink + redirectToCheckout` for full-page redirects\n- Use `loadClink + initEmbeddedCheckout + mount` for iframe embedding\n- Listen to `complete` for the business terminal state\n- Listen to `hosted_return` for host-page UI cleanup such as hiding the iframe or returning to the merchant page\n\n## Backend contracts\n\nBootstrap contract: `docs/sdk-bootstrap-contract.md`\n\nEmbedded checkout contract: `docs/sdk-embedded-backend-contract.md`\n","readmeFilename":"README.md"}