npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@clink-test/js

v1.0.0

Published

Clink hosted checkout JS SDK (V1)

Downloads

19

Readme

Clink Embedded Checkout SDK

Clink 的 Checkout SDK:

  • V1: redirectToCheckout 托管页跳转
  • V2: initEmbeddedCheckout 嵌入式 checkout

安装

# 未来发布后
npm install @clink/js
# 或 pnpm add @clink/js

# 当前仓库内本地开发
npm --prefix packages/clink-js run test
npm --prefix packages/clink-js run build

API

loadClink(publicKey, options?)

import { loadClink } from '@clink/js';

const clink = await loadClink('pk_uat_xxx', {
  checkoutEnvironment: 'uat',
  locale: 'zh-CN',
});
  • publicKey 必须匹配:pk_test_*pk_uat_*pk_prod_*
  • 推荐通过 checkoutEnvironment 让 SDK 自动请求对应环境的 Clink bootstrap,或直接传 checkoutBaseUrl
  • checkoutEnvironment 可选:uat / live
  • checkoutBaseUrl 可选:显式指定 Clink checkout 根地址,例如 https://checkout.clinkbill.com

clink.redirectToCheckout(params)(V1)

await clink.redirectToCheckout({
  sessionParam: 'sess_123#token',
  replace: false,
});

行为规则:

  • sessionParamsessionId 至少传一个。
  • 如果两者都传,优先使用 sessionParam
  • 跳转地址规则:{checkoutBaseUrl}/pay/{encodeURIComponent(sessionParam)}
  • replace: true 使用 location.replace;默认 location.assign

clink.initEmbeddedCheckout(options)(V2)

import { loadClink } from '@clink/js';

const clink = await loadClink('pk_uat_xxx');
// 推荐显式传 checkoutEnvironment 或 checkoutBaseUrl
// const clink = await loadClink('pk_uat_xxx', { checkoutEnvironment: 'uat' });

const embedded = await clink.initEmbeddedCheckout({
  fetchSession: async () => {
    const resp = await fetch('/api/checkout/session', { method: 'POST' });
    const data = await resp.json();
    return {
      checkoutUrl: data.checkoutUrl as string,
      sessionId: data.sessionId as string,
      orderId: data.orderId as string,
    };
  },
  pollStatus: async ({ orderId }) => {
    if (!orderId) return null;

    const resp = await fetch(`/api/topup/status?order_id=${orderId}`);
    const data = await resp.json();

    if (data.credited) return 'success';
    if (data.status === 'failed') return 'error';
    if (data.status === 'refunded') return 'cancelled';
    return 'pending';
  },
  onEvent(event) {
    console.log('[embedded event]', event.type, event.payload);
  },
  // default true
  autoDestroyOnComplete: true,
});

embedded.mount('#checkout');

React 接入模板:

import { useEffect, useRef, useState } from 'react';
import {
  CLINK_ERROR_CODES,
  ClinkError,
  loadClink,
  type EmbeddedCheckout,
} from '@clink/js';

interface CheckoutPageProps {
  publicKey: string;
}

export function CheckoutPage({ publicKey }: CheckoutPageProps) {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const embeddedRef = useRef<EmbeddedCheckout | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;

    async function setup() {
      try {
        const clink = await loadClink(publicKey, {
          checkoutEnvironment: 'live',
          locale: 'en-US',
        });

        const embedded = await clink.initEmbeddedCheckout({
          fetchSession: async () => {
            const resp = await fetch('/api/checkout/session', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
            });

            if (!resp.ok) {
              throw new Error('failed to create checkout session');
            }

            const data = (await resp.json()) as {
              checkoutUrl: string;
              sessionId: string;
              orderId?: string;
            };
            return data;
          },
          pollStatus: async ({ orderId }) => {
            if (!orderId) {
              return null;
            }

            const resp = await fetch(`/api/topup/status?order_id=${orderId}`);
            if (!resp.ok) {
              return null;
            }

            const data = (await resp.json()) as {
              credited?: boolean;
              status?: string;
            };

            if (data.credited || data.status === 'paid') {
              return 'success';
            }
            if (data.status === 'failed') {
              return 'error';
            }
            if (data.status === 'refunded') {
              return 'cancelled';
            }
            return 'pending';
          },
          onEvent(event) {
            if (event.type === 'hosted_return') {
              window.location.assign('/payment/result');
              return;
            }

            if (event.type === 'complete' && event.payload?.state === 'success') {
              window.location.assign('/payment/success');
              return;
            }

            if (event.type === 'error') {
              setError('Payment failed. Please try again.');
            }
          },
        });

        if (cancelled || !containerRef.current) {
          embedded.destroy();
          return;
        }

        embedded.mount(containerRef.current);
        embeddedRef.current = embedded;
        setLoading(false);
      } catch (err) {
        if (cancelled) {
          return;
        }

        if (
          err instanceof ClinkError &&
          err.code === CLINK_ERROR_CODES.SESSION_ID_FETCH_FAILED
        ) {
          setError('Unable to create checkout session.');
        } else {
          setError('Unable to load checkout.');
        }
        setLoading(false);
      }
    }

    void setup();

    return () => {
      cancelled = true;
      embeddedRef.current?.destroy();
      embeddedRef.current = null;
    };
  }, [publicKey]);

  return (
    <div>
      {loading ? <div>Loading checkout...</div> : null}
      {error ? <div role="alert">{error}</div> : null}
      <div ref={containerRef} id="checkout" />
    </div>
  );
}

React 接入建议:

  • loadClink()initEmbeddedCheckout() 放在 useEffect 里做一次初始化。
  • ref 保存 EmbeddedCheckout 实例,组件卸载时调用 destroy()
  • 业务终态建议监听 complete 事件;商户自定义成功页/取消页回跳后的 UI 收口建议监听 hosted_return
  • fetchSession() 必须返回 { checkoutUrl, sessionId, orderId? },其中 checkoutUrl 直接使用服务端 createCheckoutSession() 返回的 url
  • 默认情况下,支付成功触发 complete 后 SDK 会自动销毁 iframe;如需保留 iframe,传 autoDestroyOnComplete: false
  • fetchSession() 只调用你自己的后端,不要把 secret key 放到前端。

options:

  • fetchSession: 商户前端调用自己后端,必须返回 { checkoutUrl, sessionId, orderId? }
  • onEvent (optional): 统一事件回调
  • autoResize (optional, default true): 自动按 resize 事件更新 iframe 高度
  • autoDestroyOnComplete (optional, default true): 成功完成后自动销毁 iframe,由宿主页面接管成功态
  • pollStatus (optional): 商户后端确认型支付兜底,SDK 会按 pollIntervalMs 轮询并在终态时自动发出 complete
  • pollIntervalMs (optional, default 2000)

实例 API:

  • mount(container: string | HTMLElement)
  • unmount()
  • destroy()
  • on(type, handler)(返回取消监听函数)
  • getState() -> { mounted, destroyed }

事件类型:

  • ready
  • resize
  • state_change
  • complete
  • hosted_return
  • error

推荐心智模型:

  • complete: 支付终态事件。来自 checkout 页面本身,或 pollStatus 兜底确认后的终态。业务是否真正成功应以这个事件为准。
  • hosted_return: 商户自定义 successUrl / cancelUrl 页面回跳后的 UI 收口事件。适合关闭 iframe、返回宿主页面、展示商户自己的结果页。
  • error: SDK 或轮询过程错误,不等于支付失败终态。

V2 错误处理示例

import { ClinkError, CLINK_ERROR_CODES } from '@clink/js';

try {
  const embedded = await clink.initEmbeddedCheckout({
    fetchSession: async () => ({ checkoutUrl: '', sessionId: 'sess_xxx' }),
  });
  embedded.mount('#checkout');
} catch (error) {
  if (error instanceof ClinkError) {
    if (error.code === CLINK_ERROR_CODES.INVALID_SESSION_ID) {
      console.error('checkoutUrl 或 sessionId 无效');
    }
  }
}

bootstrap 环境控制

SDK 支持通过环境变量固定远端 bootstrap 环境:

  • CLINK_ENV=sandbox -> https://uat-api.clinkbill.com/api/sdk/bootstrap
  • CLINK_ENV=production -> https://api.clinkbill.com/api/sdk/bootstrap

优先级(高 -> 低):

  1. loadClink(..., { checkoutBaseUrl })
  2. loadClink(..., { checkoutEnvironment })
  3. CLINK_ENV

远端 bootstrap 请求格式:

POST /api/sdk/bootstrap
X-API-Key: pk_uat_xxx / pk_prod_xxx
X-Timestamp: <unix_ms>
Accept-Language: zh-CN, en-US;q=0.9
Content-Type: application/json

请求体:

{
  "origin": "https://merchant.example.com",
  "sdkVersion": "1.0.0",
  "locale": "zh_CN"
}

成功响应:

{
  "code": 200,
  "msg": "Success",
  "data": {
    "checkoutBaseUrl": "https://checkout.clinkbill.com",
    "merchantId": "mcht_xxx",
    "merchantName": "Demo Merchant",
    "environment": "Uat",
    "mode": "uat",
    "features": {
      "embeddedCheckout": true
    },
    "allowedParentOrigins": ["https://merchant.example.com"]
  }
}

错误处理

SDK 会抛出 ClinkError,包含 code 字段:

import { ClinkError, CLINK_ERROR_CODES, loadClink } from '@clink/js';

try {
  const clink = await loadClink('pk_prod_xxx');
  await clink.redirectToCheckout({ sessionId: 'sess_001' });
} catch (error) {
  if (error instanceof ClinkError) {
    if (error.code === CLINK_ERROR_CODES.INVALID_PUBLIC_KEY) {
      console.error('public key 格式不正确');
    }
  }
}

常见错误码:

  • INVALID_PUBLIC_KEY
  • INVALID_CHECKOUT_ENV
  • BOOTSTRAP_REQUEST_FAILED
  • INVALID_BOOTSTRAP_RESPONSE
  • INVALID_REDIRECT_PARAMS
  • INVALID_EMBEDDED_OPTIONS
  • INVALID_SESSION_ID
  • SESSION_ID_FETCH_FAILED
  • EMBEDDED_CHECKOUT_DISABLED
  • CONTAINER_NOT_FOUND
  • NOT_IN_BROWSER

安全边界

  • SDK 不处理卡号、CVV 等敏感支付信息。
  • SDK 不包含任何 secret key 逻辑。
  • SDK 仅使用 publicKey 调用 bootstrap。
  • V2 推荐由商户后端创建 checkout session,并返回 checkoutUrlsessionId 等元数据给前端。
  • complete 只表示 checkout / 后端确认过的终态;商户回跳页使用 hosted_return 做 UI 收口,不混用支付确认语义。

V1 -> V2 迁移说明

  • V1 代码可保持不变:loadClink + redirectToCheckout 仍可用。
  • 新接入嵌入式时,新增 initEmbeddedCheckout + mount
  • 业务终态建议监听 complete
  • 宿主页面收起 iframe、返回商户页面等 UI 收口逻辑,建议监听 hosted_return

后端契约

bootstrap 接口契约见: /Users/kanwu/IdeaProjects/customer-app/docs/sdk-v1-backend-contract.md

V2 增量契约见: /Users/kanwu/IdeaProjects/customer-app/docs/sdk-v2-backend-contract.md