@youidian/sdk 3.4.1 → 3.4.2

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/dist/index.cjs CHANGED
@@ -33,10 +33,12 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
33
33
  var src_exports = {};
34
34
  __export(src_exports, {
35
35
  LoginUI: () => LoginUI,
36
+ MessageUI: () => MessageUI,
36
37
  PaymentClient: () => PaymentClient,
37
38
  PaymentUI: () => PaymentUI,
38
39
  createFeedbackWidget: () => createFeedbackWidget,
39
40
  createLoginUI: () => createLoginUI,
41
+ createMessageUI: () => createMessageUI,
40
42
  createPaymentUI: () => createPaymentUI,
41
43
  detectLoginEnvironment: () => detectLoginEnvironment,
42
44
  getCustomAmountRechargeRule: () => getCustomAmountRechargeRule,
@@ -46,6 +48,221 @@ __export(src_exports, {
46
48
  });
47
49
  module.exports = __toCommonJS(src_exports);
48
50
 
51
+ // src/brand-logo.ts
52
+ function ensureSdkBrandLogoStyles() {
53
+ if (document.getElementById?.("youidian-sdk-brand-logo-style")) {
54
+ return;
55
+ }
56
+ const style = document.createElement("style");
57
+ style.id = "youidian-sdk-brand-logo-style";
58
+ style.textContent = `
59
+ .youidian-sdk-brand-logo__ring,
60
+ .youidian-sdk-brand-logo__dot,
61
+ .youidian-sdk-brand-logo__copy,
62
+ .youidian-sdk-brand-logo__tagline {
63
+ transform-box: fill-box;
64
+ transform-origin: center;
65
+ will-change: opacity, transform;
66
+ }
67
+
68
+ .youidian-sdk-brand-logo[data-animated="true"] .youidian-sdk-brand-logo__ring {
69
+ animation: youidian-sdk-brand-logo-ring 1400ms cubic-bezier(0.4, 0, 0.2, 1) infinite;
70
+ }
71
+
72
+ .youidian-sdk-brand-logo[data-animated="true"] .youidian-sdk-brand-logo__dot {
73
+ animation: youidian-sdk-brand-logo-dot 1400ms cubic-bezier(0.4, 0, 0.2, 1) infinite;
74
+ }
75
+
76
+ .youidian-sdk-brand-logo[data-animated="true"] .youidian-sdk-brand-logo__copy {
77
+ animation: youidian-sdk-brand-logo-copy 1600ms cubic-bezier(0.4, 0, 0.2, 1) infinite;
78
+ }
79
+
80
+ .youidian-sdk-brand-logo[data-animated="true"] .youidian-sdk-brand-logo__tagline {
81
+ animation: youidian-sdk-brand-logo-tagline 1600ms cubic-bezier(0.4, 0, 0.2, 1) infinite;
82
+ }
83
+
84
+ @keyframes youidian-sdk-brand-logo-ring {
85
+ 0%,
86
+ 100% {
87
+ opacity: 0.72;
88
+ transform: rotate(0deg) scale(0.98);
89
+ }
90
+ 50% {
91
+ opacity: 1;
92
+ transform: rotate(8deg) scale(1.02);
93
+ }
94
+ }
95
+
96
+ @keyframes youidian-sdk-brand-logo-dot {
97
+ 0%,
98
+ 100% {
99
+ opacity: 0.58;
100
+ transform: scale(0.9);
101
+ }
102
+ 50% {
103
+ opacity: 1;
104
+ transform: scale(1.08);
105
+ }
106
+ }
107
+
108
+ @keyframes youidian-sdk-brand-logo-copy {
109
+ 0%,
110
+ 100% {
111
+ opacity: 0.86;
112
+ transform: translateY(0);
113
+ }
114
+ 50% {
115
+ opacity: 1;
116
+ transform: translateY(-1px);
117
+ }
118
+ }
119
+
120
+ @keyframes youidian-sdk-brand-logo-tagline {
121
+ 0%,
122
+ 100% {
123
+ opacity: 0.64;
124
+ }
125
+ 50% {
126
+ opacity: 0.92;
127
+ }
128
+ }
129
+
130
+ @media (prefers-reduced-motion: reduce) {
131
+ .youidian-sdk-brand-logo__ring,
132
+ .youidian-sdk-brand-logo__dot,
133
+ .youidian-sdk-brand-logo__copy,
134
+ .youidian-sdk-brand-logo__tagline {
135
+ animation: none;
136
+ opacity: 1;
137
+ transform: none;
138
+ }
139
+ }
140
+ `;
141
+ document.head?.appendChild(style);
142
+ }
143
+ function createSdkBrandLogoMark({
144
+ animated,
145
+ inverted,
146
+ size,
147
+ theme,
148
+ variant = "brand"
149
+ }) {
150
+ ensureSdkBrandLogoStyles();
151
+ const mark = document.createElementNS("http://www.w3.org/2000/svg", "svg");
152
+ mark.setAttribute("viewBox", "0 0 512 512");
153
+ mark.setAttribute("fill", "none");
154
+ mark.setAttribute("aria-hidden", "true");
155
+ Object.assign(mark.style, {
156
+ width: `${size}px`,
157
+ height: `${size}px`,
158
+ color: inverted ? "#ffffff" : theme.primary,
159
+ display: "block",
160
+ flex: "0 0 auto",
161
+ overflow: "visible"
162
+ });
163
+ const logoGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
164
+ logoGroup.setAttribute(
165
+ "transform",
166
+ variant === "mark" ? "matrix(0.74 0 0 0.74 -124 -114)" : "matrix(0.58 0 0 0.58 -40 -31)"
167
+ );
168
+ const ring = document.createElementNS("http://www.w3.org/2000/svg", "path");
169
+ ring.setAttribute("class", animated ? "youidian-sdk-brand-logo__ring" : "");
170
+ ring.setAttribute("fill", inverted ? "currentColor" : theme.primary);
171
+ ring.setAttribute(
172
+ "d",
173
+ "M704.298 679.54A264 264 0 1 1 704.298 309.46A49 49 0 0 1 634.4 378.149A166 166 0 1 0 634.4 610.851A49 49 0 0 1 704.298 679.54Z"
174
+ );
175
+ const dot = document.createElementNS("http://www.w3.org/2000/svg", "circle");
176
+ dot.setAttribute("class", animated ? "youidian-sdk-brand-logo__dot" : "");
177
+ dot.setAttribute("fill", inverted ? "currentColor" : theme.secondary);
178
+ dot.setAttribute("cx", "714.5");
179
+ dot.setAttribute("cy", "490.5");
180
+ dot.setAttribute("r", "68");
181
+ dot.setAttribute("opacity", inverted ? "0.78" : "1");
182
+ logoGroup.appendChild(ring);
183
+ logoGroup.appendChild(dot);
184
+ mark.appendChild(logoGroup);
185
+ return mark;
186
+ }
187
+ function createSdkBrandLogo({
188
+ animated,
189
+ brand,
190
+ brandWidth,
191
+ copyWidth,
192
+ gap = 10,
193
+ markMarginRight,
194
+ markSize,
195
+ nameSize,
196
+ subtitleMarginTop = 6,
197
+ subtitleSize,
198
+ subtitleWeight = "800",
199
+ theme,
200
+ variant = "brand"
201
+ }) {
202
+ ensureSdkBrandLogoStyles();
203
+ const wrapper = document.createElement("div");
204
+ wrapper.className = "youidian-sdk-brand-logo";
205
+ wrapper.setAttribute("data-animated", animated ? "true" : "false");
206
+ Object.assign(wrapper.style, {
207
+ display: "inline-flex",
208
+ alignItems: "center",
209
+ justifyContent: "center",
210
+ gap: `${gap}px`,
211
+ width: brandWidth ? `${brandWidth}px` : void 0
212
+ });
213
+ const copy = document.createElement("div");
214
+ copy.className = "youidian-sdk-brand-logo__copy";
215
+ copy.setAttribute("data-brand-locale", brand.language);
216
+ Object.assign(copy.style, {
217
+ display: "flex",
218
+ flexDirection: "column",
219
+ alignItems: "flex-start",
220
+ justifyContent: "center",
221
+ lineHeight: "1",
222
+ width: copyWidth ? `${copyWidth}px` : void 0
223
+ });
224
+ const name = document.createElement("div");
225
+ name.className = "youidian-sdk-brand-logo__name";
226
+ name.textContent = brand.name;
227
+ Object.assign(name.style, {
228
+ color: theme.foreground,
229
+ fontFamily: brand.language === "zh" ? '"Noto Sans TC", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' : '"Unbounded", "Space Grotesk", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
230
+ fontSize: `${nameSize}px`,
231
+ fontWeight: brand.language === "zh" ? "900" : "800",
232
+ letterSpacing: brand.language === "zh" ? "0.08em" : "0.01em",
233
+ lineHeight: "1",
234
+ whiteSpace: "nowrap"
235
+ });
236
+ const subtitle = document.createElement("div");
237
+ subtitle.className = "youidian-sdk-brand-logo__tagline";
238
+ subtitle.textContent = brand.subtitle;
239
+ Object.assign(subtitle.style, {
240
+ color: theme.muted,
241
+ fontFamily: brand.language === "zh" ? '"Noto Sans TC", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' : '"Space Grotesk", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
242
+ fontSize: `${subtitleSize}px`,
243
+ fontWeight: subtitleWeight,
244
+ letterSpacing: brand.language === "zh" ? "0.24em" : "0.07em",
245
+ lineHeight: "1",
246
+ marginTop: `${subtitleMarginTop}px`,
247
+ textTransform: brand.language === "zh" ? "none" : "uppercase",
248
+ whiteSpace: "nowrap"
249
+ });
250
+ copy.appendChild(name);
251
+ copy.appendChild(subtitle);
252
+ const mark = createSdkBrandLogoMark({
253
+ animated,
254
+ size: markSize,
255
+ theme,
256
+ variant
257
+ });
258
+ if (typeof markMarginRight === "number") {
259
+ mark.style.marginRight = `${markMarginRight}px`;
260
+ }
261
+ wrapper.appendChild(mark);
262
+ wrapper.appendChild(copy);
263
+ return wrapper;
264
+ }
265
+
49
266
  // src/hosted-modal.ts
50
267
  var SDK_SUPPORTED_LOCALES = [
51
268
  "en",
@@ -169,7 +386,7 @@ var HostedFrameModal = class {
169
386
  const container = document.createElement("div");
170
387
  Object.assign(container.style, {
171
388
  width: options.width || "450px",
172
- height: options.height || "min(600px, 90vh)",
389
+ height: options.height || "min(760px, calc(100vh - 32px))",
173
390
  backgroundColor: "transparent",
174
391
  borderRadius: "28px",
175
392
  overflow: "visible",
@@ -177,8 +394,7 @@ var HostedFrameModal = class {
177
394
  boxShadow: "none",
178
395
  maxWidth: "calc(100vw - 32px)",
179
396
  maxHeight: "calc(100vh - 32px)",
180
- transition: "height 180ms ease",
181
- willChange: "height"
397
+ transition: "none"
182
398
  });
183
399
  const closeBtn = document.createElement("button");
184
400
  closeBtn.innerHTML = "\xD7";
@@ -1019,6 +1235,7 @@ var LoginUI = class extends HostedFrameModal {
1019
1235
  this.completed = false;
1020
1236
  }
1021
1237
  markIframeReady() {
1238
+ if (this.iframeReady) return;
1022
1239
  this.iframeReady = true;
1023
1240
  if (typeof window !== "undefined" && this.iframeLoadTimer) {
1024
1241
  window.clearTimeout(this.iframeLoadTimer);
@@ -1263,6 +1480,17 @@ var LoginUI = class extends HostedFrameModal {
1263
1480
  },
1264
1481
  onMessage: (data, _container, event) => {
1265
1482
  const isIframeEvent = event.source === this.iframe?.contentWindow;
1483
+ if (!isIframeEvent && (data.type === "LOGIN_READY" || data.type === "LOGIN_RESIZE")) {
1484
+ logLoginWarn(
1485
+ "Login lifecycle event ignored: event.source does not match iframe contentWindow",
1486
+ {
1487
+ type: data.type,
1488
+ eventOrigin: event.origin,
1489
+ hasIframe: Boolean(this.iframe),
1490
+ hasIframeContentWindow: Boolean(this.iframe?.contentWindow)
1491
+ }
1492
+ );
1493
+ }
1266
1494
  if (isIframeEvent && (data.type === "LOGIN_READY" || data.type === "LOGIN_RESIZE") || data.type === "LOGIN_SUCCESS" || data.type === "LOGIN_ERROR") {
1267
1495
  this.markIframeReady();
1268
1496
  }
@@ -1397,6 +1625,406 @@ function createLoginUI() {
1397
1625
  handleLoginCallbackIfPresent({ autoClose: false });
1398
1626
 
1399
1627
  // src/client.ts
1628
+ var DEFAULT_PAYMENT_IFRAME_LOAD_TIMEOUT_MS = 8e3;
1629
+ var PAYMENT_UI_LOG_PREFIX = "[Youidian PaymentUI]";
1630
+ function hexToRgb(value) {
1631
+ const normalized = value.trim().replace(/^#/, "");
1632
+ if (!/^[0-9a-f]{3}([0-9a-f]{3})?$/i.test(normalized)) return null;
1633
+ const hex = normalized.length === 3 ? normalized.split("").map((char) => `${char}${char}`).join("") : normalized;
1634
+ const number = Number.parseInt(hex, 16);
1635
+ return {
1636
+ b: number & 255,
1637
+ g: number >> 8 & 255,
1638
+ r: number >> 16 & 255
1639
+ };
1640
+ }
1641
+ function alphaColor(value, alpha) {
1642
+ const rgb = hexToRgb(value);
1643
+ if (!rgb)
1644
+ return `color-mix(in srgb, ${value} ${Math.round(alpha * 100)}%, transparent)`;
1645
+ return `rgba(${rgb.r},${rgb.g},${rgb.b},${alpha})`;
1646
+ }
1647
+ function resolvePaymentTheme(options) {
1648
+ const primary = options?.theme?.primary || "#16a34a";
1649
+ const secondary = options?.theme?.secondary || "color-mix(in srgb, #16a34a 78%, white)";
1650
+ const foreground = options?.theme?.foreground || "#0f172a";
1651
+ const muted = options?.theme?.muted || "#64748b";
1652
+ const surface = options?.theme?.surface || "#ffffff";
1653
+ return {
1654
+ foreground,
1655
+ muted,
1656
+ primary,
1657
+ primary10: alphaColor(primary, 0.1),
1658
+ primary14: alphaColor(primary, 0.14),
1659
+ primary20: alphaColor(primary, 0.2),
1660
+ primary28: alphaColor(primary, 0.28),
1661
+ primary36: alphaColor(primary, 0.36),
1662
+ primary64: alphaColor(primary, 0.64),
1663
+ secondary,
1664
+ surface
1665
+ };
1666
+ }
1667
+ function logPaymentInfo(message, details) {
1668
+ if (typeof console === "undefined") return;
1669
+ console.info(PAYMENT_UI_LOG_PREFIX, message, details || {});
1670
+ }
1671
+ function logPaymentWarn(message, details) {
1672
+ if (typeof console === "undefined") return;
1673
+ console.warn(PAYMENT_UI_LOG_PREFIX, message, details || {});
1674
+ }
1675
+ function getPaymentIframeLoadTimeoutMs(options) {
1676
+ const timeout = options?.iframeLoadTimeoutMs;
1677
+ if (typeof timeout !== "number" || !Number.isFinite(timeout)) {
1678
+ return DEFAULT_PAYMENT_IFRAME_LOAD_TIMEOUT_MS;
1679
+ }
1680
+ return Math.max(0, timeout);
1681
+ }
1682
+ function isChinesePaymentLocale(locale) {
1683
+ return locale?.toLowerCase().startsWith("zh") || false;
1684
+ }
1685
+ function getPaymentFallbackText(options) {
1686
+ const isChinese = isChinesePaymentLocale(options?.locale);
1687
+ return {
1688
+ buttonText: options?.fallbackButtonText || (isChinese ? "\u5728\u65B0\u9875\u9762\u4E2D\u6253\u5F00" : "Open in new page"),
1689
+ description: isChinese ? "\u5F53\u524D\u7F51\u7EDC\u53EF\u80FD\u8F83\u6162\uFF0C\u5EFA\u8BAE\u91CD\u65B0\u5C1D\u8BD5\u6216\u5728\u65B0\u9875\u9762\u4E2D\u6253\u5F00\uFF0C\u4EE5\u83B7\u5F97\u66F4\u6D41\u7545\u7684\u652F\u4ED8\u4F53\u9A8C\u3002" : "Your network may be slow. Try again or open the payment page in a new page to continue.",
1690
+ retryText: options?.fallbackRetryText || (isChinese ? "\u91CD\u65B0\u5C1D\u8BD5" : "Try again"),
1691
+ title: isChinese ? "\u652F\u4ED8\u9875\u9762\u6253\u5F00\u8F83\u6162" : "Payment page is taking longer"
1692
+ };
1693
+ }
1694
+ function getPaymentBrandText(options) {
1695
+ const isChinese = isChinesePaymentLocale(options?.locale);
1696
+ return {
1697
+ language: isChinese ? "zh" : "en",
1698
+ name: isChinese ? "\u4F18\u6613\u70B9" : "Youidian",
1699
+ subtitle: isChinese ? "\u5F00\u53D1\u8005\u670D\u52A1\u5E73\u53F0" : "Developer Service Platform"
1700
+ };
1701
+ }
1702
+ function getPaymentBrandLoadingText(options) {
1703
+ const isChinese = isChinesePaymentLocale(options?.locale);
1704
+ return {
1705
+ language: isChinese ? "zh" : "en",
1706
+ name: isChinese ? "\u4F18\u6613\u70B9" : "Youidian",
1707
+ subtitle: isChinese ? "\u5F00\u53D1\u8005\u670D\u52A1\u4E2D\u53F0" : "Developer Service"
1708
+ };
1709
+ }
1710
+ function getPaymentLoadingText(options) {
1711
+ const isChinese = isChinesePaymentLocale(options?.locale);
1712
+ return options?.loadingTitle || options?.loadingDescription || (isChinese ? "\u52A0\u8F7D\u4E2D..." : "Loading...");
1713
+ }
1714
+ function applyPaymentPanelStyle(panel, theme) {
1715
+ Object.assign(panel.style, {
1716
+ position: "absolute",
1717
+ inset: "0",
1718
+ display: "flex",
1719
+ alignItems: "center",
1720
+ justifyContent: "center",
1721
+ borderRadius: "28px",
1722
+ background: `linear-gradient(180deg, ${theme.surface} 0%, rgba(248,250,252,0.98) 100%)`,
1723
+ color: theme.foreground,
1724
+ padding: "28px 24px",
1725
+ textAlign: "center",
1726
+ zIndex: "1",
1727
+ boxShadow: "none",
1728
+ backdropFilter: "blur(16px)"
1729
+ });
1730
+ }
1731
+ function createPaymentIcon(type) {
1732
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1733
+ svg.setAttribute("viewBox", "0 0 24 24");
1734
+ svg.setAttribute("fill", "none");
1735
+ svg.setAttribute("aria-hidden", "true");
1736
+ Object.assign(svg.style, {
1737
+ display: "block",
1738
+ flex: "0 0 auto"
1739
+ });
1740
+ const paths = type === "external" ? [
1741
+ "M7 17L17 7",
1742
+ "M10 7H17V14",
1743
+ "M6.5 6.5H5.5A2.5 2.5 0 0 0 3 9V18.5A2.5 2.5 0 0 0 5.5 21H15A2.5 2.5 0 0 0 17.5 18.5V17.5"
1744
+ ] : type === "refresh" ? [
1745
+ "M20 6V11H15",
1746
+ "M4 18V13H9",
1747
+ "M18.2 9A7 7 0 0 0 6.2 6.6L4 9",
1748
+ "M5.8 15A7 7 0 0 0 17.8 17.4L20 15"
1749
+ ] : ["M12 7V12L15 14", "M21 12A9 9 0 1 1 3 12A9 9 0 0 1 21 12"];
1750
+ for (const value of paths) {
1751
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1752
+ path.setAttribute("d", value);
1753
+ path.setAttribute("stroke", "currentColor");
1754
+ path.setAttribute("stroke-width", "2");
1755
+ path.setAttribute("stroke-linecap", "round");
1756
+ path.setAttribute("stroke-linejoin", "round");
1757
+ svg.appendChild(path);
1758
+ }
1759
+ return svg;
1760
+ }
1761
+ function createPaymentFallbackArtwork(theme) {
1762
+ const artwork = document.createElement("div");
1763
+ Object.assign(artwork.style, {
1764
+ height: "164px",
1765
+ margin: "0 auto 34px",
1766
+ position: "relative",
1767
+ width: "184px"
1768
+ });
1769
+ const wash = document.createElement("div");
1770
+ Object.assign(wash.style, {
1771
+ position: "absolute",
1772
+ left: "16px",
1773
+ top: "20px",
1774
+ width: "152px",
1775
+ height: "126px",
1776
+ borderRadius: "48% 52% 43% 57% / 54% 42% 58% 46%",
1777
+ background: `radial-gradient(circle at 48% 42%, ${theme.primary20}, ${theme.primary10} 52%, transparent 76%)`,
1778
+ filter: "blur(1px)",
1779
+ transform: "rotate(-10deg)"
1780
+ });
1781
+ const blob = document.createElement("div");
1782
+ Object.assign(blob.style, {
1783
+ position: "absolute",
1784
+ left: "26px",
1785
+ top: "28px",
1786
+ width: "132px",
1787
+ height: "116px",
1788
+ borderRadius: "42% 58% 53% 47% / 45% 39% 61% 55%",
1789
+ background: `linear-gradient(150deg, ${theme.primary14}, ${theme.primary10})`,
1790
+ boxShadow: `0 30px 64px ${theme.primary14}`,
1791
+ transform: "rotate(8deg)"
1792
+ });
1793
+ const tile = document.createElement("div");
1794
+ Object.assign(tile.style, {
1795
+ position: "absolute",
1796
+ left: "50%",
1797
+ top: "50%",
1798
+ transform: "translate(-50%, -50%) rotate(-4deg)",
1799
+ width: "112px",
1800
+ height: "106px",
1801
+ borderRadius: "38% 62% 50% 50% / 44% 43% 57% 56%",
1802
+ background: `linear-gradient(145deg, ${theme.primary}, ${theme.secondary})`,
1803
+ border: `1px solid ${theme.primary36}`,
1804
+ color: "#ffffff",
1805
+ display: "inline-flex",
1806
+ alignItems: "center",
1807
+ justifyContent: "center",
1808
+ boxShadow: `0 26px 54px ${theme.primary28}`
1809
+ });
1810
+ const icon = createPaymentIcon("slow");
1811
+ Object.assign(icon.style, {
1812
+ width: "52px",
1813
+ height: "52px",
1814
+ transform: "rotate(4deg)"
1815
+ });
1816
+ tile.appendChild(icon);
1817
+ artwork.appendChild(wash);
1818
+ artwork.appendChild(blob);
1819
+ artwork.appendChild(tile);
1820
+ return artwork;
1821
+ }
1822
+ function createPaymentBrandLoading(options, theme) {
1823
+ const brand = getPaymentBrandLoadingText(options);
1824
+ const wrapper = document.createElement("div");
1825
+ Object.assign(wrapper.style, {
1826
+ display: "flex",
1827
+ flexDirection: "column",
1828
+ alignItems: "center",
1829
+ justifyContent: "center",
1830
+ margin: "0 auto 16px"
1831
+ });
1832
+ wrapper.appendChild(
1833
+ createSdkBrandLogo({
1834
+ animated: true,
1835
+ brand,
1836
+ brandWidth: 292,
1837
+ copyWidth: 176,
1838
+ gap: 6,
1839
+ markMarginRight: -10,
1840
+ markSize: 112,
1841
+ nameSize: 39,
1842
+ subtitleMarginTop: 8,
1843
+ subtitleSize: 14,
1844
+ subtitleWeight: "700",
1845
+ theme,
1846
+ variant: "brand"
1847
+ })
1848
+ );
1849
+ return wrapper;
1850
+ }
1851
+ function createPaymentPlatformLogo(options, theme) {
1852
+ const brand = getPaymentBrandText(options);
1853
+ const wrapper = document.createElement("div");
1854
+ Object.assign(wrapper.style, {
1855
+ display: "flex",
1856
+ alignItems: "center",
1857
+ justifyContent: "center",
1858
+ margin: "0 auto 10px"
1859
+ });
1860
+ wrapper.appendChild(
1861
+ createSdkBrandLogo({
1862
+ brand,
1863
+ markSize: 48,
1864
+ nameSize: brand.language === "zh" ? 22 : 22,
1865
+ subtitleSize: brand.language === "zh" ? 9 : 9,
1866
+ theme
1867
+ })
1868
+ );
1869
+ return wrapper;
1870
+ }
1871
+ function createPaymentPanelContent() {
1872
+ const content = document.createElement("div");
1873
+ Object.assign(content.style, {
1874
+ display: "flex",
1875
+ flexDirection: "column",
1876
+ justifyContent: "center",
1877
+ maxWidth: "364px",
1878
+ minHeight: "100%",
1879
+ width: "100%"
1880
+ });
1881
+ return content;
1882
+ }
1883
+ function createPaymentPanelTitle(value, theme) {
1884
+ const title = document.createElement("div");
1885
+ title.textContent = value;
1886
+ Object.assign(title.style, {
1887
+ color: theme?.foreground || "#0f172a",
1888
+ fontSize: "24px",
1889
+ fontWeight: "900",
1890
+ lineHeight: "1.22",
1891
+ marginBottom: "16px"
1892
+ });
1893
+ return title;
1894
+ }
1895
+ function createPaymentPanelDescription(value, theme) {
1896
+ const description = document.createElement("div");
1897
+ description.textContent = value;
1898
+ Object.assign(description.style, {
1899
+ color: theme?.muted || "#64748b",
1900
+ fontSize: "14px",
1901
+ fontWeight: "600",
1902
+ lineHeight: "1.62",
1903
+ margin: "0 auto",
1904
+ maxWidth: "320px"
1905
+ });
1906
+ return description;
1907
+ }
1908
+ function createPaymentLoadingDescription(value, theme) {
1909
+ const description = document.createElement("div");
1910
+ description.textContent = value;
1911
+ Object.assign(description.style, {
1912
+ color: theme?.muted || "#64748b",
1913
+ fontSize: "14px",
1914
+ fontWeight: "500",
1915
+ lineHeight: "1.25",
1916
+ margin: "12px auto 0",
1917
+ maxWidth: "320px"
1918
+ });
1919
+ return description;
1920
+ }
1921
+ function createPaymentFallbackMessage(options) {
1922
+ const message = document.createElement("div");
1923
+ Object.assign(message.style, {
1924
+ margin: "0 auto",
1925
+ textAlign: "center"
1926
+ });
1927
+ message.appendChild(createPaymentFallbackArtwork(options.theme));
1928
+ message.appendChild(createPaymentPanelTitle(options.title, options.theme));
1929
+ message.appendChild(
1930
+ createPaymentPanelDescription(options.description, options.theme)
1931
+ );
1932
+ return message;
1933
+ }
1934
+ function createPaymentLoadingPanel(options) {
1935
+ const panel = document.createElement("div");
1936
+ const theme = resolvePaymentTheme(options);
1937
+ applyPaymentPanelStyle(panel, theme);
1938
+ const content = createPaymentPanelContent();
1939
+ content.appendChild(createPaymentBrandLoading(options, theme));
1940
+ content.appendChild(
1941
+ createPaymentLoadingDescription(getPaymentLoadingText(options), theme)
1942
+ );
1943
+ panel.appendChild(content);
1944
+ return panel;
1945
+ }
1946
+ function createPaymentButton(value, options) {
1947
+ const primary = options?.primary || false;
1948
+ const theme = options?.theme || resolvePaymentTheme();
1949
+ const button = document.createElement("button");
1950
+ button.type = "button";
1951
+ Object.assign(button.style, {
1952
+ width: "100%",
1953
+ height: "50px",
1954
+ borderRadius: "14px",
1955
+ border: primary ? `1px solid ${theme.primary}` : `1px solid ${theme.primary64}`,
1956
+ background: primary ? `linear-gradient(135deg, ${theme.primary}, ${theme.secondary})` : "#ffffff",
1957
+ color: primary ? "#ffffff" : theme.primary,
1958
+ cursor: "pointer",
1959
+ display: "inline-flex",
1960
+ alignItems: "center",
1961
+ justifyContent: "center",
1962
+ gap: "10px",
1963
+ fontSize: "15px",
1964
+ fontWeight: "800",
1965
+ marginTop: primary ? "12px" : "0",
1966
+ boxShadow: primary ? `0 14px 28px ${theme.primary20}` : "none"
1967
+ });
1968
+ if (options?.icon) {
1969
+ const icon = createPaymentIcon(options.icon);
1970
+ Object.assign(icon.style, {
1971
+ width: "17px",
1972
+ height: "17px"
1973
+ });
1974
+ button.appendChild(icon);
1975
+ }
1976
+ const label = document.createElement("span");
1977
+ label.textContent = value;
1978
+ button.appendChild(label);
1979
+ return button;
1980
+ }
1981
+ function createPaymentFallbackPanel(options) {
1982
+ const panel = document.createElement("div");
1983
+ const theme = resolvePaymentTheme(options.paymentOptions);
1984
+ applyPaymentPanelStyle(panel, theme);
1985
+ const content = createPaymentPanelContent();
1986
+ Object.assign(content.style, {
1987
+ justifyContent: "space-between"
1988
+ });
1989
+ const main = document.createElement("div");
1990
+ Object.assign(main.style, {
1991
+ display: "flex",
1992
+ flexDirection: "column",
1993
+ alignItems: "center",
1994
+ marginTop: "38px",
1995
+ width: "100%"
1996
+ });
1997
+ main.appendChild(
1998
+ createPaymentFallbackMessage({
1999
+ description: options.description,
2000
+ theme,
2001
+ title: options.title
2002
+ })
2003
+ );
2004
+ const actions = document.createElement("div");
2005
+ Object.assign(actions.style, {
2006
+ marginTop: "46px",
2007
+ width: "100%"
2008
+ });
2009
+ const retryButton = createPaymentButton(options.retryText, {
2010
+ icon: "refresh",
2011
+ theme
2012
+ });
2013
+ retryButton.onclick = options.onRetry;
2014
+ actions.appendChild(retryButton);
2015
+ const openButton = createPaymentButton(options.buttonText, {
2016
+ icon: "external",
2017
+ primary: true,
2018
+ theme
2019
+ });
2020
+ openButton.onclick = options.onOpen;
2021
+ actions.appendChild(openButton);
2022
+ main.appendChild(actions);
2023
+ content.appendChild(main);
2024
+ content.appendChild(createPaymentPlatformLogo(options.paymentOptions, theme));
2025
+ panel.appendChild(content);
2026
+ return panel;
2027
+ }
1400
2028
  var PaymentUI = class extends HostedFrameModal {
1401
2029
  constructor() {
1402
2030
  super(...arguments);
@@ -1405,6 +2033,127 @@ var PaymentUI = class extends HostedFrameModal {
1405
2033
  __publicField(this, "activeOrderId", null);
1406
2034
  __publicField(this, "paymentCompleted", false);
1407
2035
  __publicField(this, "cancelRequestedOrderId", null);
2036
+ __publicField(this, "iframeFallbackPanel", null);
2037
+ __publicField(this, "iframeLoadingPanel", null);
2038
+ __publicField(this, "iframeLoadTimer", null);
2039
+ __publicField(this, "iframeReady", false);
2040
+ }
2041
+ cleanupPaymentFrameState() {
2042
+ if (typeof window !== "undefined" && this.iframeLoadTimer) {
2043
+ window.clearTimeout(this.iframeLoadTimer);
2044
+ }
2045
+ if (this.iframeFallbackPanel?.parentNode) {
2046
+ this.iframeFallbackPanel.parentNode.removeChild(this.iframeFallbackPanel);
2047
+ }
2048
+ if (this.iframeLoadingPanel?.parentNode) {
2049
+ this.iframeLoadingPanel.parentNode.removeChild(this.iframeLoadingPanel);
2050
+ }
2051
+ this.iframeFallbackPanel = null;
2052
+ this.iframeLoadingPanel = null;
2053
+ this.iframeLoadTimer = null;
2054
+ this.iframeReady = false;
2055
+ }
2056
+ markPaymentIframeReady() {
2057
+ if (this.iframeReady) return;
2058
+ this.iframeReady = true;
2059
+ if (typeof window !== "undefined" && this.iframeLoadTimer) {
2060
+ window.clearTimeout(this.iframeLoadTimer);
2061
+ this.iframeLoadTimer = null;
2062
+ }
2063
+ if (this.iframeFallbackPanel?.parentNode) {
2064
+ this.iframeFallbackPanel.parentNode.removeChild(this.iframeFallbackPanel);
2065
+ }
2066
+ if (this.iframeLoadingPanel?.parentNode) {
2067
+ this.iframeLoadingPanel.parentNode.removeChild(this.iframeLoadingPanel);
2068
+ }
2069
+ this.iframeFallbackPanel = null;
2070
+ this.iframeLoadingPanel = null;
2071
+ }
2072
+ showPaymentIframeLoading(container, options) {
2073
+ if (this.iframeLoadingPanel || this.iframeReady) return;
2074
+ const panel = createPaymentLoadingPanel(options);
2075
+ this.iframeLoadingPanel = panel;
2076
+ container.appendChild(panel);
2077
+ }
2078
+ openFallbackPaymentPage(finalUrl, options) {
2079
+ if (typeof window === "undefined") return;
2080
+ const popup = window.open(
2081
+ finalUrl,
2082
+ "youidian-payment-checkout",
2083
+ "popup=yes,width=480,height=760,resizable=yes,scrollbars=yes"
2084
+ );
2085
+ if (popup) {
2086
+ logPaymentInfo("Opened hosted checkout fallback page", {
2087
+ checkoutUrl: finalUrl
2088
+ });
2089
+ options?.onFallbackOpen?.(finalUrl);
2090
+ try {
2091
+ popup.focus();
2092
+ } catch {
2093
+ }
2094
+ return;
2095
+ }
2096
+ logPaymentWarn("Hosted checkout fallback popup was blocked", {
2097
+ checkoutUrl: finalUrl
2098
+ });
2099
+ options?.onFallbackBlocked?.(finalUrl);
2100
+ window.location.assign(finalUrl);
2101
+ }
2102
+ showPaymentIframeFallback(options) {
2103
+ if (this.iframeReady || this.iframeFallbackPanel) return;
2104
+ const { container, finalUrl, paymentOptions } = options;
2105
+ logPaymentWarn("Hosted checkout iframe did not report ready in time", {
2106
+ checkoutUrl: finalUrl
2107
+ });
2108
+ if (this.iframeLoadingPanel?.parentNode) {
2109
+ this.iframeLoadingPanel.parentNode.removeChild(this.iframeLoadingPanel);
2110
+ }
2111
+ this.iframeLoadingPanel = null;
2112
+ const fallbackText = getPaymentFallbackText(paymentOptions);
2113
+ const panel = createPaymentFallbackPanel({
2114
+ buttonText: fallbackText.buttonText,
2115
+ description: fallbackText.description,
2116
+ onOpen: () => {
2117
+ this.openFallbackPaymentPage(finalUrl, paymentOptions);
2118
+ },
2119
+ onRetry: () => {
2120
+ this.iframeFallbackPanel = null;
2121
+ if (panel.parentNode) {
2122
+ panel.parentNode.removeChild(panel);
2123
+ }
2124
+ this.iframeReady = false;
2125
+ this.showPaymentIframeLoading(container, paymentOptions);
2126
+ if (this.iframe) {
2127
+ this.iframe.src = finalUrl;
2128
+ }
2129
+ this.startPaymentIframeWatchdog({
2130
+ container,
2131
+ finalUrl,
2132
+ paymentOptions
2133
+ });
2134
+ },
2135
+ paymentOptions,
2136
+ retryText: fallbackText.retryText,
2137
+ title: fallbackText.title
2138
+ });
2139
+ this.iframeFallbackPanel = panel;
2140
+ container.appendChild(panel);
2141
+ paymentOptions?.onFallbackVisible?.();
2142
+ }
2143
+ startPaymentIframeWatchdog(options) {
2144
+ if (typeof window === "undefined") return;
2145
+ if (this.iframeLoadTimer) {
2146
+ window.clearTimeout(this.iframeLoadTimer);
2147
+ }
2148
+ const timeoutMs = getPaymentIframeLoadTimeoutMs(options.paymentOptions);
2149
+ if (timeoutMs === 0) {
2150
+ this.showPaymentIframeFallback(options);
2151
+ return;
2152
+ }
2153
+ this.iframeLoadTimer = window.setTimeout(() => {
2154
+ this.iframeLoadTimer = null;
2155
+ this.showPaymentIframeFallback(options);
2156
+ }, timeoutMs);
1408
2157
  }
1409
2158
  cancelHostedOrder(reason = "user_cancel", orderId) {
1410
2159
  const targetOrderId = orderId || this.activeOrderId;
@@ -1447,6 +2196,7 @@ var PaymentUI = class extends HostedFrameModal {
1447
2196
  this.activeOrderId = null;
1448
2197
  this.paymentCompleted = false;
1449
2198
  this.cancelRequestedOrderId = null;
2199
+ this.cleanupPaymentFrameState();
1450
2200
  if (typeof urlOrParams === "string") {
1451
2201
  checkoutUrl = urlOrParams;
1452
2202
  try {
@@ -1515,14 +2265,21 @@ var PaymentUI = class extends HostedFrameModal {
1515
2265
  checkoutUrl,
1516
2266
  getAutoResolvedLocale(options?.locale)
1517
2267
  );
1518
- this.openHostedFrame(finalUrl, {
2268
+ const hostedFrame = this.openHostedFrame(finalUrl, {
1519
2269
  allowedOrigin: options?.allowedOrigin,
2270
+ height: "min(760px, calc(100vh - 32px))",
1520
2271
  onCloseButton: () => {
1521
2272
  this.cancelHostedOrder("modal_close");
1522
2273
  options?.onCancel?.(this.activeOrderId || void 0);
1523
2274
  },
1524
- onMessage: (data, container) => {
2275
+ onMessage: (data, container, event) => {
2276
+ const isIframeMessage = event.source === this.iframe?.contentWindow;
2277
+ if (isIframeMessage && (data.type === "PAYMENT_READY" || data.type === "PAYMENT_RESIZE" || data.type === "PAYMENT_STARTED")) {
2278
+ this.markPaymentIframeReady();
2279
+ }
1525
2280
  switch (data.type) {
2281
+ case "PAYMENT_READY":
2282
+ break;
1526
2283
  case "PAYMENT_STARTED":
1527
2284
  if (data.orderId) {
1528
2285
  this.activeOrderId = data.orderId;
@@ -1537,11 +2294,6 @@ var PaymentUI = class extends HostedFrameModal {
1537
2294
  options?.onCancel?.(data.orderId);
1538
2295
  break;
1539
2296
  case "PAYMENT_RESIZE":
1540
- if (data.height) {
1541
- const maxHeight = window.innerHeight * 0.9;
1542
- const newHeight = Math.min(data.height, maxHeight);
1543
- container.style.height = `${newHeight}px`;
1544
- }
1545
2297
  break;
1546
2298
  case "PAYMENT_CLOSE":
1547
2299
  this.cancelHostedOrder("payment_close", data.orderId);
@@ -1549,8 +2301,21 @@ var PaymentUI = class extends HostedFrameModal {
1549
2301
  options?.onClose?.();
1550
2302
  break;
1551
2303
  }
1552
- }
2304
+ },
2305
+ width: "min(450px, 100%)"
1553
2306
  });
2307
+ if (hostedFrame) {
2308
+ this.showPaymentIframeLoading(hostedFrame.container, options);
2309
+ this.startPaymentIframeWatchdog({
2310
+ container: hostedFrame.container,
2311
+ finalUrl,
2312
+ paymentOptions: options
2313
+ });
2314
+ }
2315
+ }
2316
+ close() {
2317
+ super.close();
2318
+ this.cleanupPaymentFrameState();
1554
2319
  }
1555
2320
  /**
1556
2321
  * Poll order status from integrator's API endpoint
@@ -1596,6 +2361,29 @@ var PaymentUI = class extends HostedFrameModal {
1596
2361
  });
1597
2362
  }
1598
2363
  };
2364
+ var MessageUI = class {
2365
+ openWechatBinding(options) {
2366
+ const bindingUrl = options.bindingUrl?.trim();
2367
+ if (!bindingUrl) {
2368
+ throw new Error("bindingUrl is required");
2369
+ }
2370
+ if (options.displayMode === "popup") {
2371
+ const popup = window.open(
2372
+ bindingUrl,
2373
+ options.popupName || "youidian-wechat-message-binding",
2374
+ "width=480,height=720,noopener,noreferrer"
2375
+ );
2376
+ if (!popup) {
2377
+ window.location.href = bindingUrl;
2378
+ }
2379
+ return;
2380
+ }
2381
+ window.location.href = bindingUrl;
2382
+ }
2383
+ };
2384
+ function createMessageUI() {
2385
+ return new MessageUI();
2386
+ }
1599
2387
  function createPaymentUI() {
1600
2388
  return new PaymentUI();
1601
2389
  }
@@ -2381,6 +3169,12 @@ var PaymentClient = class {
2381
3169
  }
2382
3170
  /**
2383
3171
  * Decrypts the callback notification payload using AES-256-GCM.
3172
+ *
3173
+ * @deprecated This legacy encrypted-envelope protocol is NOT what the platform
3174
+ * actually delivers. The platform sends an HMAC-signed plaintext JSON webhook
3175
+ * with `X-UniPay-Signature` / `X-UniPay-Timestamp` headers; use
3176
+ * {@link PaymentClient.verifyWebhookSignature} to validate those instead.
3177
+ *
2384
3178
  * @param notification - The encrypted notification from payment webhook
2385
3179
  * @returns Decrypted payment callback data
2386
3180
  */
@@ -2403,6 +3197,39 @@ var PaymentClient = class {
2403
3197
  );
2404
3198
  }
2405
3199
  }
3200
+ /**
3201
+ * Verify an inbound platform webhook signature.
3202
+ *
3203
+ * The platform delivers webhooks as plaintext JSON with two headers:
3204
+ * `X-UniPay-Signature` (HMAC-SHA256 of `${timestamp}.${rawBodyString}`) and
3205
+ * `X-UniPay-Timestamp`. The body itself is NOT encrypted; verify the signature
3206
+ * then `JSON.parse` the body to obtain a {@link WebhookPayload}.
3207
+ *
3208
+ * @param rawBody - The raw request body string (exactly as received)
3209
+ * @param signature - The `X-UniPay-Signature` header value (hex)
3210
+ * @param timestamp - The `X-UniPay-Timestamp` header value (ms epoch string)
3211
+ * @param options - Optional `toleranceMs` for timestamp freshness (default 5 min)
3212
+ * @returns `true` if the signature is valid and the timestamp is fresh
3213
+ */
3214
+ verifyWebhookSignature(rawBody, signature, timestamp, options) {
3215
+ try {
3216
+ const toleranceMs = options?.toleranceMs ?? 5 * 60 * 1e3;
3217
+ const timestampMs = Number.parseInt(timestamp, 10);
3218
+ if (Number.isNaN(timestampMs) || Math.abs(Date.now() - timestampMs) > toleranceMs) {
3219
+ return false;
3220
+ }
3221
+ const rawString = `${timestamp}.${rawBody}`;
3222
+ const expected = import_crypto.default.createHmac("sha256", this.appSecret).update(rawString).digest("hex");
3223
+ const signatureBuffer = Buffer.from(signature);
3224
+ const expectedBuffer = Buffer.from(expected);
3225
+ if (signatureBuffer.length !== expectedBuffer.length) {
3226
+ return false;
3227
+ }
3228
+ return import_crypto.default.timingSafeEqual(signatureBuffer, expectedBuffer);
3229
+ } catch {
3230
+ return false;
3231
+ }
3232
+ }
2406
3233
  /**
2407
3234
  * Fetch products for the configured app.
2408
3235
  */
@@ -2609,15 +3436,11 @@ var PaymentClient = class {
2609
3436
  * Useful when subscription credits and perpetual credits use separate keys.
2610
3437
  */
2611
3438
  async consumeEntitlementPool(userId, keys, amount, options) {
2612
- return this.request(
2613
- "POST",
2614
- `/users/${userId}/entitlements/consume-pool`,
2615
- {
2616
- keys,
2617
- amount,
2618
- ...options
2619
- }
2620
- );
3439
+ return this.request("POST", `/users/${userId}/entitlements/consume-pool`, {
3440
+ keys,
3441
+ amount,
3442
+ ...options
3443
+ });
2621
3444
  }
2622
3445
  /**
2623
3446
  * Get active numeric entitlement buckets ordered by expiration.
@@ -2709,14 +3532,43 @@ var PaymentClient = class {
2709
3532
  }
2710
3533
  );
2711
3534
  }
3535
+ /**
3536
+ * Create a hosted WeChat official account binding URL for a user.
3537
+ */
3538
+ async createWechatMessageBinding(params) {
3539
+ const loginExternalUserId = params.loginExternalUserId?.trim() || params.userId?.trim();
3540
+ const merchantUserId = params.merchantUserId?.trim();
3541
+ if (!loginExternalUserId && !merchantUserId) {
3542
+ throw new Error("userId or merchantUserId is required");
3543
+ }
3544
+ return this.request("POST", "/messages/wechat/bindings", {
3545
+ ...params,
3546
+ loginExternalUserId,
3547
+ merchantUserId
3548
+ });
3549
+ }
3550
+ /**
3551
+ * Send an app-linked message template to a bound recipient.
3552
+ */
3553
+ async sendMessage(params) {
3554
+ if (!params.templateId?.trim() && !params.templateCode?.trim()) {
3555
+ throw new Error("templateId or templateCode is required");
3556
+ }
3557
+ if (!params.data || typeof params.data !== "object") {
3558
+ throw new Error("data is required");
3559
+ }
3560
+ return this.request("POST", "/messages/send", params);
3561
+ }
2712
3562
  };
2713
3563
  // Annotate the CommonJS export names for ESM import in node:
2714
3564
  0 && (module.exports = {
2715
3565
  LoginUI,
3566
+ MessageUI,
2716
3567
  PaymentClient,
2717
3568
  PaymentUI,
2718
3569
  createFeedbackWidget,
2719
3570
  createLoginUI,
3571
+ createMessageUI,
2720
3572
  createPaymentUI,
2721
3573
  detectLoginEnvironment,
2722
3574
  getCustomAmountRechargeRule,