@triplinkintl/embedded-card 1.0.0
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/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/index.cjs +174 -0
- package/dist/index.d.cts +95 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +154 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TripLink
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# TripLink Embedded Card
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
[TripLink](https://www.triplinkintl.com/) Embedded Card lets you show virtual card details in your own product, without taking on PCI scope. [TripLink](https://www.triplinkintl.com/) remains the regulated issuer and PCI payment provider — issuing, funding, and protecting card data on your behalf. A few lines of JavaScript is enough to embed a TripLink-hosted card into any page.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @triplinkintl/embedded-card
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import TripLink from '@triplinkintl/embedded-card';
|
|
17
|
+
|
|
18
|
+
const { destroy } = TripLink.renderCard({
|
|
19
|
+
container: 'card-root',
|
|
20
|
+
ephemeralKey: auth.ephemeralKey,
|
|
21
|
+
cardLid: auth.cardLogId,
|
|
22
|
+
customerId: auth.customerId,
|
|
23
|
+
source: 'blink',
|
|
24
|
+
locale: 'zh-HK',
|
|
25
|
+
env: 'FAT',
|
|
26
|
+
styles: {
|
|
27
|
+
root: { backgroundColor: '#0b1f3a' },
|
|
28
|
+
},
|
|
29
|
+
onSuccess: ({ requestId }) => {
|
|
30
|
+
console.log('card rendered', requestId);
|
|
31
|
+
},
|
|
32
|
+
onError: ({ resultCode, message, requestId }) => {
|
|
33
|
+
console.error(resultCode, message, requestId);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// later
|
|
38
|
+
destroy();
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<div id="card-root" style="width: 360px; height: 220px;"></div>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Named export `renderCard` is equivalent to `TripLink.renderCard`.
|
|
46
|
+
|
|
47
|
+
## Options
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
| Field | Type | Required | Default | Description |
|
|
51
|
+
| -------------- | ---------------------------------------------- | -------- | ------- | ------------------------------------------- |
|
|
52
|
+
| `container` | `string` | yes | — | DOM container id |
|
|
53
|
+
| `ephemeralKey` | `string` | yes | — | Ephemeral key from TripLink |
|
|
54
|
+
| `cardLid` | `string` | yes | — | Card id (`cardLogId`) from TripLink |
|
|
55
|
+
| `customerId` | `string` | yes | — | Customer id from TripLink |
|
|
56
|
+
| `source` | `string` | yes | — | Partner source. Apply to TripLink first. |
|
|
57
|
+
| `locale` | `string` | no | `en-US` | BCP 47 language tag, e.g. `en-HK`, `zh-HK` |
|
|
58
|
+
| `env` | `FAT` / `PROD` | no | `PROD` | Environment. `FAT` or `PROD`. |
|
|
59
|
+
| `styles` | `{ root? }` | no | — | Card styles sent to the iframe via CardInit |
|
|
60
|
+
| `onSuccess` | `({ requestId }) => void` | no | — | Fired when the iframe reports init success |
|
|
61
|
+
| `onError` | `({ resultCode, message, requestId }) => void` | no | — | Fired on validation / load / init errors |
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
## Result codes
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
| `resultCode` | When |
|
|
70
|
+
| --------------------- | ----------------------------------------- |
|
|
71
|
+
| `BROWSER_VERSION_LOW` | Browser is too old |
|
|
72
|
+
| `INVALID_PARAMS` | Missing or invalid SDK arguments |
|
|
73
|
+
| `CONTAINER_NOT_FOUND` | No DOM node with the given `container` id |
|
|
74
|
+
| `IFRAME_LOAD_FAILED` | Iframe failed to load |
|
|
75
|
+
| `CARD_INIT_FAILED` | Iframe reported card init failure |
|
|
76
|
+
|
|
77
|
+
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: !0 });
|
|
9
|
+
}, __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from == "object" || typeof from == "function")
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
!__hasOwnProp.call(to, key) && key !== except && __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
return to;
|
|
14
|
+
};
|
|
15
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: !0 }), mod);
|
|
16
|
+
|
|
17
|
+
// src/index.ts
|
|
18
|
+
var index_exports = {};
|
|
19
|
+
__export(index_exports, {
|
|
20
|
+
DEFAULT_ENV: () => DEFAULT_ENV,
|
|
21
|
+
DEFAULT_LOCALE: () => DEFAULT_LOCALE,
|
|
22
|
+
IFRAME_BASE_URLS: () => IFRAME_BASE_URLS,
|
|
23
|
+
ResultCode: () => ResultCode,
|
|
24
|
+
TripLink: () => TripLink,
|
|
25
|
+
default: () => index_default,
|
|
26
|
+
renderCard: () => renderCard
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/renderCard.ts
|
|
31
|
+
var import_uuid = require("uuid");
|
|
32
|
+
|
|
33
|
+
// src/constants.ts
|
|
34
|
+
var DEFAULT_LOCALE = "en-US", DEFAULT_ENV = "PROD", IFRAME_BASE_URLS = {
|
|
35
|
+
FAT: "https://vcc-hosting.fat.ctripqa.com/embedded-card/cardInfo",
|
|
36
|
+
PROD: "https://vcc-hosting.fat.ctripqa.com/embedded-card/cardInfo"
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/enums/resultCode.ts
|
|
40
|
+
var ResultCode = /* @__PURE__ */ ((ResultCode2) => (ResultCode2.INVALID_PARAMS = "INVALID_PARAMS", ResultCode2.CONTAINER_NOT_FOUND = "CONTAINER_NOT_FOUND", ResultCode2.IFRAME_LOAD_FAILED = "IFRAME_LOAD_FAILED", ResultCode2.CARD_INIT_FAILED = "CARD_INIT_FAILED", ResultCode2.BROWSER_VERSION_LOW = "BROWSER_VERSION_LOW", ResultCode2))(ResultCode || {});
|
|
41
|
+
|
|
42
|
+
// src/utils/validators.ts
|
|
43
|
+
var REQUIRED_STRING_KEYS = ["container", "ephemeralKey", "cardLid", "customerId", "source"];
|
|
44
|
+
function isNonEmptyString(value) {
|
|
45
|
+
return typeof value == "string" && value.trim().length > 0;
|
|
46
|
+
}
|
|
47
|
+
function isSupported() {
|
|
48
|
+
return [
|
|
49
|
+
"postMessage",
|
|
50
|
+
// Portal/SDK 需要支持 postMessage
|
|
51
|
+
"TextEncoder",
|
|
52
|
+
// Portal 需要支持 TextEncoder
|
|
53
|
+
"Uint8Array",
|
|
54
|
+
// Portal 需要支持 Uint8Array
|
|
55
|
+
"atob"
|
|
56
|
+
// Portal 需要支持 atob
|
|
57
|
+
].every((key) => {
|
|
58
|
+
let isSupported3 = typeof window[key] == "function";
|
|
59
|
+
return isSupported3 || console.warn(`[TripLink SDK] ${key} is not supported`), isSupported3;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function validateOptions(options) {
|
|
63
|
+
if (!options || typeof options != "object")
|
|
64
|
+
return { ok: !1, code: "INVALID_PARAMS" /* INVALID_PARAMS */, message: "options is required" };
|
|
65
|
+
for (let key of REQUIRED_STRING_KEYS)
|
|
66
|
+
if (!isNonEmptyString(options[key]))
|
|
67
|
+
return { ok: !1, code: "INVALID_PARAMS" /* INVALID_PARAMS */, message: `${key} is required` };
|
|
68
|
+
return options.env != null && options.env !== "FAT" && options.env !== "PROD" ? { ok: !1, code: "INVALID_PARAMS" /* INVALID_PARAMS */, message: "env must be FAT or PROD" } : { ok: !0 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/utils/url.ts
|
|
72
|
+
function appendQuery(base, params) {
|
|
73
|
+
let pairs = [];
|
|
74
|
+
for (let key in params) {
|
|
75
|
+
if (!Object.prototype.hasOwnProperty.call(params, key))
|
|
76
|
+
continue;
|
|
77
|
+
let value = params[key];
|
|
78
|
+
value && pairs.push(key + "=" + encodeURIComponent(value));
|
|
79
|
+
}
|
|
80
|
+
if (!pairs.length)
|
|
81
|
+
return base;
|
|
82
|
+
let hashIndex = base.indexOf("#"), hash = hashIndex >= 0 ? base.slice(hashIndex) : "", withoutHash = hashIndex >= 0 ? base.slice(0, hashIndex) : base, lastChar = withoutHash.charAt(withoutHash.length - 1), separator = "&";
|
|
83
|
+
return withoutHash.indexOf("?") === -1 ? separator = "?" : (lastChar === "?" || lastChar === "&") && (separator = ""), withoutHash + separator + pairs.join("&") + hash;
|
|
84
|
+
}
|
|
85
|
+
function getOrigin(url) {
|
|
86
|
+
let match = url.match(/^(https?:\/\/[^/?#]+)/i);
|
|
87
|
+
return match ? match[1] : "";
|
|
88
|
+
}
|
|
89
|
+
function buildIframeUrl(options) {
|
|
90
|
+
return appendQuery(IFRAME_BASE_URLS[options.env || DEFAULT_ENV], {
|
|
91
|
+
locale: options.locale,
|
|
92
|
+
source: options.source,
|
|
93
|
+
oid: options.oid
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/renderCard.ts
|
|
98
|
+
function applyIframeStyle(iframe) {
|
|
99
|
+
iframe.setAttribute("title", "TripLink Card"), iframe.setAttribute("allow", "clipboard-write");
|
|
100
|
+
}
|
|
101
|
+
function renderCard(options) {
|
|
102
|
+
let noop = { destroy() {
|
|
103
|
+
} }, requestId = (0, import_uuid.v4)(), reportError = (code, message) => {
|
|
104
|
+
console.error(`[Triplink-embedded-card SDK] ${code}: ${message}`), options?.onError?.({ resultCode: code, message, requestId });
|
|
105
|
+
};
|
|
106
|
+
if (!isSupported())
|
|
107
|
+
return reportError("BROWSER_VERSION_LOW" /* BROWSER_VERSION_LOW */, "Browser version is too low"), noop;
|
|
108
|
+
let validated = validateOptions(options);
|
|
109
|
+
if (!validated.ok)
|
|
110
|
+
return reportError(validated.code, validated.message), noop;
|
|
111
|
+
let container = document.getElementById(options.container);
|
|
112
|
+
if (!container)
|
|
113
|
+
return reportError("CONTAINER_NOT_FOUND" /* CONTAINER_NOT_FOUND */, `Mount node not found: ${options.container}`), noop;
|
|
114
|
+
let locale = (options.locale || DEFAULT_LOCALE).trim(), env = options.env || DEFAULT_ENV, iframeSrc = buildIframeUrl({
|
|
115
|
+
env,
|
|
116
|
+
locale,
|
|
117
|
+
source: options.source.trim(),
|
|
118
|
+
oid: requestId
|
|
119
|
+
}), iframeOrigin = getOrigin(iframeSrc), iframe = document.createElement("iframe");
|
|
120
|
+
applyIframeStyle(iframe);
|
|
121
|
+
let didInit = !1, destroyed = !1, onComplete = () => didInit || destroyed ? !1 : didInit = !0, onError = (code, message) => {
|
|
122
|
+
onComplete() && reportError(code, message);
|
|
123
|
+
}, onSuccess = () => {
|
|
124
|
+
onComplete() && options.onSuccess?.({ requestId });
|
|
125
|
+
}, postCardInitMessage = () => {
|
|
126
|
+
destroyed || didInit || iframe.contentWindow?.postMessage(
|
|
127
|
+
{
|
|
128
|
+
requestId,
|
|
129
|
+
type: "CARD_INIT" /* CardInit */,
|
|
130
|
+
payload: {
|
|
131
|
+
ephemeralKey: options.ephemeralKey.trim(),
|
|
132
|
+
cardLogId: options.cardLid.trim(),
|
|
133
|
+
customerId: options.customerId.trim(),
|
|
134
|
+
...options.styles ? { styles: options.styles } : {}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
iframeOrigin
|
|
138
|
+
);
|
|
139
|
+
}, onMessage = (event) => {
|
|
140
|
+
if (destroyed || didInit || iframe.contentWindow && event.source !== iframe.contentWindow || event.origin !== iframeOrigin)
|
|
141
|
+
return;
|
|
142
|
+
let { requestId: messageRequestId, type: messageType, payload: messagePayload } = event.data || {};
|
|
143
|
+
if (!(!messageRequestId || messageRequestId !== requestId))
|
|
144
|
+
switch (messageType) {
|
|
145
|
+
case "READY" /* Ready */:
|
|
146
|
+
postCardInitMessage();
|
|
147
|
+
break;
|
|
148
|
+
case "CARD_INIT_SUCCESS" /* InitSuccess */:
|
|
149
|
+
onSuccess();
|
|
150
|
+
break;
|
|
151
|
+
case "CARD_INIT_FAILED" /* InitFailed */:
|
|
152
|
+
let message = messagePayload?.message || "Failed to initialize card information";
|
|
153
|
+
onError("CARD_INIT_FAILED" /* CARD_INIT_FAILED */, message);
|
|
154
|
+
break;
|
|
155
|
+
default:
|
|
156
|
+
onError("CARD_INIT_FAILED" /* CARD_INIT_FAILED */, `Unknown message type '${messageType}'`);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}, onIframeError = () => {
|
|
160
|
+
onError("IFRAME_LOAD_FAILED" /* IFRAME_LOAD_FAILED */, "Failed to load iframe page");
|
|
161
|
+
};
|
|
162
|
+
return window.addEventListener("message", onMessage), iframe.addEventListener("error", onIframeError), iframe.src = iframeSrc, container.replaceChildren(iframe), {
|
|
163
|
+
/** 移除 iframe,并停止 message 监听。 */
|
|
164
|
+
destroy() {
|
|
165
|
+
destroyed || (destroyed = !0, window.removeEventListener("message", onMessage), iframe.removeEventListener("error", onIframeError), iframe.remove());
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/index.ts
|
|
171
|
+
var TripLink = {
|
|
172
|
+
renderCard
|
|
173
|
+
}, index_default = TripLink;
|
|
174
|
+
//# sourceMappingURL=index.cjs.map
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** 传给 `onError({ resultCode, message, requestId })` 的错误码。 */
|
|
2
|
+
declare enum ResultCode {
|
|
3
|
+
/** 无效的参数 */
|
|
4
|
+
INVALID_PARAMS = "INVALID_PARAMS",
|
|
5
|
+
/** 容器未找到 */
|
|
6
|
+
CONTAINER_NOT_FOUND = "CONTAINER_NOT_FOUND",
|
|
7
|
+
/** iframe 加载失败 */
|
|
8
|
+
IFRAME_LOAD_FAILED = "IFRAME_LOAD_FAILED",
|
|
9
|
+
/** 卡初始化失败 */
|
|
10
|
+
CARD_INIT_FAILED = "CARD_INIT_FAILED",
|
|
11
|
+
/** 浏览器版本低(不支持 postMessage) */
|
|
12
|
+
BROWSER_VERSION_LOW = "BROWSER_VERSION_LOW"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** iframe 环境:FAT 或 PROD。 */
|
|
16
|
+
type Env = 'FAT' | 'PROD';
|
|
17
|
+
/** `ResultCode` 的字符串取值。 */
|
|
18
|
+
type ResultCodeValue = ResultCode;
|
|
19
|
+
/** `onError` 回调入参。 */
|
|
20
|
+
type OnErrorPayload = {
|
|
21
|
+
/** 错误码 */
|
|
22
|
+
resultCode: ResultCode;
|
|
23
|
+
/** 错误描述 */
|
|
24
|
+
message: string;
|
|
25
|
+
/** 本次 `renderCard` 的请求 id */
|
|
26
|
+
requestId: string;
|
|
27
|
+
};
|
|
28
|
+
/** `onSuccess` 回调入参。 */
|
|
29
|
+
type OnSuccessPayload = {
|
|
30
|
+
/** 本次 `renderCard` 的请求 id */
|
|
31
|
+
requestId: string;
|
|
32
|
+
};
|
|
33
|
+
type OnError = (payload: OnErrorPayload) => void;
|
|
34
|
+
type OnSuccess = (payload: OnSuccessPayload) => void;
|
|
35
|
+
/** 传给托管页的内联样式,使用驼峰 CSS 属性名,如 `backgroundColor`。 */
|
|
36
|
+
type CSSStyleProperties = Record<string, string | number>;
|
|
37
|
+
/** 卡片样式,通过 CardInit 下发给托管页。 */
|
|
38
|
+
type CardStyles = {
|
|
39
|
+
/** 根节点样式 */
|
|
40
|
+
root?: CSSStyleProperties;
|
|
41
|
+
};
|
|
42
|
+
type RenderCardOptions = {
|
|
43
|
+
/** DOM 容器 id */
|
|
44
|
+
container: string;
|
|
45
|
+
/** TripLink 签发的短期凭证,通过 postMessage 传递,不得出现在 iframe URL 上 */
|
|
46
|
+
ephemeralKey: string;
|
|
47
|
+
/** 卡 id(cardLogId) */
|
|
48
|
+
cardLid: string;
|
|
49
|
+
/** 客户 id */
|
|
50
|
+
customerId: string;
|
|
51
|
+
/**
|
|
52
|
+
* 接入方标识,作为非敏感 query 拼到 iframe URL,
|
|
53
|
+
* 供托管页拉取对应的 origin 白名单。
|
|
54
|
+
*/
|
|
55
|
+
source: string;
|
|
56
|
+
/** BCP 47 语言标签,默认 `en-US` */
|
|
57
|
+
locale?: string;
|
|
58
|
+
/** FAT 或 PROD,默认 `PROD` */
|
|
59
|
+
env?: Env;
|
|
60
|
+
/** 卡片样式,通过 CardInit postMessage 下发给托管页 */
|
|
61
|
+
styles?: CardStyles;
|
|
62
|
+
/** iframe 初始化成功时回调 */
|
|
63
|
+
onSuccess?: OnSuccess;
|
|
64
|
+
/** 参数校验、iframe 加载或卡初始化失败时回调 */
|
|
65
|
+
onError?: OnError;
|
|
66
|
+
};
|
|
67
|
+
type RenderCardInstance = {
|
|
68
|
+
/** 移除 iframe,并停止 message 监听。 */
|
|
69
|
+
destroy: () => void;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 在宿主容器中挂载 TripLink 卡信息 iframe,并通过 postMessage 完成初始化。
|
|
74
|
+
*
|
|
75
|
+
* 流程:校验入参 → 定位容器 → 拼接不含敏感信息的 iframe URL →
|
|
76
|
+
* 挂载 iframe → 加载完成后下发 `{ ephemeralKey, cardLid, customerId }` →
|
|
77
|
+
* 等待 iframe 回传(`onSuccess` / `onError`)。
|
|
78
|
+
*
|
|
79
|
+
* @returns `{ destroy }` 用于移除 iframe 并停止监听
|
|
80
|
+
*/
|
|
81
|
+
declare function renderCard(options: RenderCardOptions): RenderCardInstance;
|
|
82
|
+
|
|
83
|
+
/** 默认语言 */
|
|
84
|
+
declare const DEFAULT_LOCALE = "en-US";
|
|
85
|
+
/** 默认环境 */
|
|
86
|
+
declare const DEFAULT_ENV: Env;
|
|
87
|
+
/** 托管卡页地址 TODO */
|
|
88
|
+
declare const IFRAME_BASE_URLS: Record<Env, string>;
|
|
89
|
+
|
|
90
|
+
/** 对外 SDK 命名空间:`TripLink.renderCard(...)`。 */
|
|
91
|
+
declare const TripLink: {
|
|
92
|
+
renderCard: typeof renderCard;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export { type CSSStyleProperties, type CardStyles, DEFAULT_ENV, DEFAULT_LOCALE, type Env, IFRAME_BASE_URLS, type OnError, type OnErrorPayload, type OnSuccess, type OnSuccessPayload, type RenderCardInstance, type RenderCardOptions, ResultCode, type ResultCodeValue, TripLink, TripLink as default, renderCard };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** 传给 `onError({ resultCode, message, requestId })` 的错误码。 */
|
|
2
|
+
declare enum ResultCode {
|
|
3
|
+
/** 无效的参数 */
|
|
4
|
+
INVALID_PARAMS = "INVALID_PARAMS",
|
|
5
|
+
/** 容器未找到 */
|
|
6
|
+
CONTAINER_NOT_FOUND = "CONTAINER_NOT_FOUND",
|
|
7
|
+
/** iframe 加载失败 */
|
|
8
|
+
IFRAME_LOAD_FAILED = "IFRAME_LOAD_FAILED",
|
|
9
|
+
/** 卡初始化失败 */
|
|
10
|
+
CARD_INIT_FAILED = "CARD_INIT_FAILED",
|
|
11
|
+
/** 浏览器版本低(不支持 postMessage) */
|
|
12
|
+
BROWSER_VERSION_LOW = "BROWSER_VERSION_LOW"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** iframe 环境:FAT 或 PROD。 */
|
|
16
|
+
type Env = 'FAT' | 'PROD';
|
|
17
|
+
/** `ResultCode` 的字符串取值。 */
|
|
18
|
+
type ResultCodeValue = ResultCode;
|
|
19
|
+
/** `onError` 回调入参。 */
|
|
20
|
+
type OnErrorPayload = {
|
|
21
|
+
/** 错误码 */
|
|
22
|
+
resultCode: ResultCode;
|
|
23
|
+
/** 错误描述 */
|
|
24
|
+
message: string;
|
|
25
|
+
/** 本次 `renderCard` 的请求 id */
|
|
26
|
+
requestId: string;
|
|
27
|
+
};
|
|
28
|
+
/** `onSuccess` 回调入参。 */
|
|
29
|
+
type OnSuccessPayload = {
|
|
30
|
+
/** 本次 `renderCard` 的请求 id */
|
|
31
|
+
requestId: string;
|
|
32
|
+
};
|
|
33
|
+
type OnError = (payload: OnErrorPayload) => void;
|
|
34
|
+
type OnSuccess = (payload: OnSuccessPayload) => void;
|
|
35
|
+
/** 传给托管页的内联样式,使用驼峰 CSS 属性名,如 `backgroundColor`。 */
|
|
36
|
+
type CSSStyleProperties = Record<string, string | number>;
|
|
37
|
+
/** 卡片样式,通过 CardInit 下发给托管页。 */
|
|
38
|
+
type CardStyles = {
|
|
39
|
+
/** 根节点样式 */
|
|
40
|
+
root?: CSSStyleProperties;
|
|
41
|
+
};
|
|
42
|
+
type RenderCardOptions = {
|
|
43
|
+
/** DOM 容器 id */
|
|
44
|
+
container: string;
|
|
45
|
+
/** TripLink 签发的短期凭证,通过 postMessage 传递,不得出现在 iframe URL 上 */
|
|
46
|
+
ephemeralKey: string;
|
|
47
|
+
/** 卡 id(cardLogId) */
|
|
48
|
+
cardLid: string;
|
|
49
|
+
/** 客户 id */
|
|
50
|
+
customerId: string;
|
|
51
|
+
/**
|
|
52
|
+
* 接入方标识,作为非敏感 query 拼到 iframe URL,
|
|
53
|
+
* 供托管页拉取对应的 origin 白名单。
|
|
54
|
+
*/
|
|
55
|
+
source: string;
|
|
56
|
+
/** BCP 47 语言标签,默认 `en-US` */
|
|
57
|
+
locale?: string;
|
|
58
|
+
/** FAT 或 PROD,默认 `PROD` */
|
|
59
|
+
env?: Env;
|
|
60
|
+
/** 卡片样式,通过 CardInit postMessage 下发给托管页 */
|
|
61
|
+
styles?: CardStyles;
|
|
62
|
+
/** iframe 初始化成功时回调 */
|
|
63
|
+
onSuccess?: OnSuccess;
|
|
64
|
+
/** 参数校验、iframe 加载或卡初始化失败时回调 */
|
|
65
|
+
onError?: OnError;
|
|
66
|
+
};
|
|
67
|
+
type RenderCardInstance = {
|
|
68
|
+
/** 移除 iframe,并停止 message 监听。 */
|
|
69
|
+
destroy: () => void;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 在宿主容器中挂载 TripLink 卡信息 iframe,并通过 postMessage 完成初始化。
|
|
74
|
+
*
|
|
75
|
+
* 流程:校验入参 → 定位容器 → 拼接不含敏感信息的 iframe URL →
|
|
76
|
+
* 挂载 iframe → 加载完成后下发 `{ ephemeralKey, cardLid, customerId }` →
|
|
77
|
+
* 等待 iframe 回传(`onSuccess` / `onError`)。
|
|
78
|
+
*
|
|
79
|
+
* @returns `{ destroy }` 用于移除 iframe 并停止监听
|
|
80
|
+
*/
|
|
81
|
+
declare function renderCard(options: RenderCardOptions): RenderCardInstance;
|
|
82
|
+
|
|
83
|
+
/** 默认语言 */
|
|
84
|
+
declare const DEFAULT_LOCALE = "en-US";
|
|
85
|
+
/** 默认环境 */
|
|
86
|
+
declare const DEFAULT_ENV: Env;
|
|
87
|
+
/** 托管卡页地址 TODO */
|
|
88
|
+
declare const IFRAME_BASE_URLS: Record<Env, string>;
|
|
89
|
+
|
|
90
|
+
/** 对外 SDK 命名空间:`TripLink.renderCard(...)`。 */
|
|
91
|
+
declare const TripLink: {
|
|
92
|
+
renderCard: typeof renderCard;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export { type CSSStyleProperties, type CardStyles, DEFAULT_ENV, DEFAULT_LOCALE, type Env, IFRAME_BASE_URLS, type OnError, type OnErrorPayload, type OnSuccess, type OnSuccessPayload, type RenderCardInstance, type RenderCardOptions, ResultCode, type ResultCodeValue, TripLink, TripLink as default, renderCard };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// src/renderCard.ts
|
|
2
|
+
import { v4 as uuidv4 } from "uuid";
|
|
3
|
+
|
|
4
|
+
// src/constants.ts
|
|
5
|
+
var DEFAULT_LOCALE = "en-US", DEFAULT_ENV = "PROD", IFRAME_BASE_URLS = {
|
|
6
|
+
FAT: "https://vcc-hosting.fat.ctripqa.com/embedded-card/cardInfo",
|
|
7
|
+
PROD: "https://vcc-hosting.fat.ctripqa.com/embedded-card/cardInfo"
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// src/enums/resultCode.ts
|
|
11
|
+
var ResultCode = /* @__PURE__ */ ((ResultCode2) => (ResultCode2.INVALID_PARAMS = "INVALID_PARAMS", ResultCode2.CONTAINER_NOT_FOUND = "CONTAINER_NOT_FOUND", ResultCode2.IFRAME_LOAD_FAILED = "IFRAME_LOAD_FAILED", ResultCode2.CARD_INIT_FAILED = "CARD_INIT_FAILED", ResultCode2.BROWSER_VERSION_LOW = "BROWSER_VERSION_LOW", ResultCode2))(ResultCode || {});
|
|
12
|
+
|
|
13
|
+
// src/utils/validators.ts
|
|
14
|
+
var REQUIRED_STRING_KEYS = ["container", "ephemeralKey", "cardLid", "customerId", "source"];
|
|
15
|
+
function isNonEmptyString(value) {
|
|
16
|
+
return typeof value == "string" && value.trim().length > 0;
|
|
17
|
+
}
|
|
18
|
+
function isSupported() {
|
|
19
|
+
return [
|
|
20
|
+
"postMessage",
|
|
21
|
+
// Portal/SDK 需要支持 postMessage
|
|
22
|
+
"TextEncoder",
|
|
23
|
+
// Portal 需要支持 TextEncoder
|
|
24
|
+
"Uint8Array",
|
|
25
|
+
// Portal 需要支持 Uint8Array
|
|
26
|
+
"atob"
|
|
27
|
+
// Portal 需要支持 atob
|
|
28
|
+
].every((key) => {
|
|
29
|
+
let isSupported3 = typeof window[key] == "function";
|
|
30
|
+
return isSupported3 || console.warn(`[TripLink SDK] ${key} is not supported`), isSupported3;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function validateOptions(options) {
|
|
34
|
+
if (!options || typeof options != "object")
|
|
35
|
+
return { ok: !1, code: "INVALID_PARAMS" /* INVALID_PARAMS */, message: "options is required" };
|
|
36
|
+
for (let key of REQUIRED_STRING_KEYS)
|
|
37
|
+
if (!isNonEmptyString(options[key]))
|
|
38
|
+
return { ok: !1, code: "INVALID_PARAMS" /* INVALID_PARAMS */, message: `${key} is required` };
|
|
39
|
+
return options.env != null && options.env !== "FAT" && options.env !== "PROD" ? { ok: !1, code: "INVALID_PARAMS" /* INVALID_PARAMS */, message: "env must be FAT or PROD" } : { ok: !0 };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/utils/url.ts
|
|
43
|
+
function appendQuery(base, params) {
|
|
44
|
+
let pairs = [];
|
|
45
|
+
for (let key in params) {
|
|
46
|
+
if (!Object.prototype.hasOwnProperty.call(params, key))
|
|
47
|
+
continue;
|
|
48
|
+
let value = params[key];
|
|
49
|
+
value && pairs.push(key + "=" + encodeURIComponent(value));
|
|
50
|
+
}
|
|
51
|
+
if (!pairs.length)
|
|
52
|
+
return base;
|
|
53
|
+
let hashIndex = base.indexOf("#"), hash = hashIndex >= 0 ? base.slice(hashIndex) : "", withoutHash = hashIndex >= 0 ? base.slice(0, hashIndex) : base, lastChar = withoutHash.charAt(withoutHash.length - 1), separator = "&";
|
|
54
|
+
return withoutHash.indexOf("?") === -1 ? separator = "?" : (lastChar === "?" || lastChar === "&") && (separator = ""), withoutHash + separator + pairs.join("&") + hash;
|
|
55
|
+
}
|
|
56
|
+
function getOrigin(url) {
|
|
57
|
+
let match = url.match(/^(https?:\/\/[^/?#]+)/i);
|
|
58
|
+
return match ? match[1] : "";
|
|
59
|
+
}
|
|
60
|
+
function buildIframeUrl(options) {
|
|
61
|
+
return appendQuery(IFRAME_BASE_URLS[options.env || DEFAULT_ENV], {
|
|
62
|
+
locale: options.locale,
|
|
63
|
+
source: options.source,
|
|
64
|
+
oid: options.oid
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/renderCard.ts
|
|
69
|
+
function applyIframeStyle(iframe) {
|
|
70
|
+
iframe.setAttribute("title", "TripLink Card"), iframe.setAttribute("allow", "clipboard-write");
|
|
71
|
+
}
|
|
72
|
+
function renderCard(options) {
|
|
73
|
+
let noop = { destroy() {
|
|
74
|
+
} }, requestId = uuidv4(), reportError = (code, message) => {
|
|
75
|
+
console.error(`[Triplink-embedded-card SDK] ${code}: ${message}`), options?.onError?.({ resultCode: code, message, requestId });
|
|
76
|
+
};
|
|
77
|
+
if (!isSupported())
|
|
78
|
+
return reportError("BROWSER_VERSION_LOW" /* BROWSER_VERSION_LOW */, "Browser version is too low"), noop;
|
|
79
|
+
let validated = validateOptions(options);
|
|
80
|
+
if (!validated.ok)
|
|
81
|
+
return reportError(validated.code, validated.message), noop;
|
|
82
|
+
let container = document.getElementById(options.container);
|
|
83
|
+
if (!container)
|
|
84
|
+
return reportError("CONTAINER_NOT_FOUND" /* CONTAINER_NOT_FOUND */, `Mount node not found: ${options.container}`), noop;
|
|
85
|
+
let locale = (options.locale || DEFAULT_LOCALE).trim(), env = options.env || DEFAULT_ENV, iframeSrc = buildIframeUrl({
|
|
86
|
+
env,
|
|
87
|
+
locale,
|
|
88
|
+
source: options.source.trim(),
|
|
89
|
+
oid: requestId
|
|
90
|
+
}), iframeOrigin = getOrigin(iframeSrc), iframe = document.createElement("iframe");
|
|
91
|
+
applyIframeStyle(iframe);
|
|
92
|
+
let didInit = !1, destroyed = !1, onComplete = () => didInit || destroyed ? !1 : didInit = !0, onError = (code, message) => {
|
|
93
|
+
onComplete() && reportError(code, message);
|
|
94
|
+
}, onSuccess = () => {
|
|
95
|
+
onComplete() && options.onSuccess?.({ requestId });
|
|
96
|
+
}, postCardInitMessage = () => {
|
|
97
|
+
destroyed || didInit || iframe.contentWindow?.postMessage(
|
|
98
|
+
{
|
|
99
|
+
requestId,
|
|
100
|
+
type: "CARD_INIT" /* CardInit */,
|
|
101
|
+
payload: {
|
|
102
|
+
ephemeralKey: options.ephemeralKey.trim(),
|
|
103
|
+
cardLogId: options.cardLid.trim(),
|
|
104
|
+
customerId: options.customerId.trim(),
|
|
105
|
+
...options.styles ? { styles: options.styles } : {}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
iframeOrigin
|
|
109
|
+
);
|
|
110
|
+
}, onMessage = (event) => {
|
|
111
|
+
if (destroyed || didInit || iframe.contentWindow && event.source !== iframe.contentWindow || event.origin !== iframeOrigin)
|
|
112
|
+
return;
|
|
113
|
+
let { requestId: messageRequestId, type: messageType, payload: messagePayload } = event.data || {};
|
|
114
|
+
if (!(!messageRequestId || messageRequestId !== requestId))
|
|
115
|
+
switch (messageType) {
|
|
116
|
+
case "READY" /* Ready */:
|
|
117
|
+
postCardInitMessage();
|
|
118
|
+
break;
|
|
119
|
+
case "CARD_INIT_SUCCESS" /* InitSuccess */:
|
|
120
|
+
onSuccess();
|
|
121
|
+
break;
|
|
122
|
+
case "CARD_INIT_FAILED" /* InitFailed */:
|
|
123
|
+
let message = messagePayload?.message || "Failed to initialize card information";
|
|
124
|
+
onError("CARD_INIT_FAILED" /* CARD_INIT_FAILED */, message);
|
|
125
|
+
break;
|
|
126
|
+
default:
|
|
127
|
+
onError("CARD_INIT_FAILED" /* CARD_INIT_FAILED */, `Unknown message type '${messageType}'`);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}, onIframeError = () => {
|
|
131
|
+
onError("IFRAME_LOAD_FAILED" /* IFRAME_LOAD_FAILED */, "Failed to load iframe page");
|
|
132
|
+
};
|
|
133
|
+
return window.addEventListener("message", onMessage), iframe.addEventListener("error", onIframeError), iframe.src = iframeSrc, container.replaceChildren(iframe), {
|
|
134
|
+
/** 移除 iframe,并停止 message 监听。 */
|
|
135
|
+
destroy() {
|
|
136
|
+
destroyed || (destroyed = !0, window.removeEventListener("message", onMessage), iframe.removeEventListener("error", onIframeError), iframe.remove());
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/index.ts
|
|
142
|
+
var TripLink = {
|
|
143
|
+
renderCard
|
|
144
|
+
}, index_default = TripLink;
|
|
145
|
+
export {
|
|
146
|
+
DEFAULT_ENV,
|
|
147
|
+
DEFAULT_LOCALE,
|
|
148
|
+
IFRAME_BASE_URLS,
|
|
149
|
+
ResultCode,
|
|
150
|
+
TripLink,
|
|
151
|
+
index_default as default,
|
|
152
|
+
renderCard
|
|
153
|
+
};
|
|
154
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@triplinkintl/embedded-card",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "TripLink Embedded Card lets you show virtual card details in your own product, without taking on PCI scope.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "q_yao",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public",
|
|
9
|
+
"registry": "https://registry.npmjs.org"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"triplink",
|
|
13
|
+
"embedded-card",
|
|
14
|
+
"vcc",
|
|
15
|
+
"iframe",
|
|
16
|
+
"PCI"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "./dist/index.cjs",
|
|
20
|
+
"module": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js",
|
|
26
|
+
"require": "./dist/index.cjs"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"!dist/ares.2019",
|
|
32
|
+
"!dist/ares.2019/**",
|
|
33
|
+
"!dist/**/*.map",
|
|
34
|
+
"!dist/**/*.global.js",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest"
|
|
42
|
+
},
|
|
43
|
+
"sideEffects": false,
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"jsdom": "^26.1.0",
|
|
46
|
+
"tsup": "^8.5.0",
|
|
47
|
+
"typescript": "^5.9.2",
|
|
48
|
+
"vitest": "^3.2.4"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"uuid": "14.0.2"
|
|
52
|
+
}
|
|
53
|
+
}
|