@youidian/sdk 3.3.2 → 3.3.4

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
@@ -37,7 +37,9 @@ __export(src_exports, {
37
37
  PaymentUI: () => PaymentUI,
38
38
  createLoginUI: () => createLoginUI,
39
39
  createPaymentUI: () => createPaymentUI,
40
- handleLoginCallbackIfPresent: () => handleLoginCallbackIfPresent
40
+ getCustomAmountRechargeRule: () => getCustomAmountRechargeRule,
41
+ handleLoginCallbackIfPresent: () => handleLoginCallbackIfPresent,
42
+ validateCustomAmountRecharge: () => validateCustomAmountRecharge
41
43
  });
42
44
  module.exports = __toCommonJS(src_exports);
43
45
 
@@ -143,8 +145,8 @@ var HostedFrameModal = class {
143
145
  __publicField(this, "messageHandler", null);
144
146
  }
145
147
  openHostedFrame(url, options) {
146
- if (typeof document === "undefined") return;
147
- if (this.modal) return;
148
+ if (typeof document === "undefined") return null;
149
+ if (this.modal) return null;
148
150
  this.modal = document.createElement("div");
149
151
  Object.assign(this.modal.style, {
150
152
  position: "fixed",
@@ -221,9 +223,13 @@ var HostedFrameModal = class {
221
223
  if (!data || typeof data !== "object" || !data.type) {
222
224
  return;
223
225
  }
224
- options.onMessage(data, container);
226
+ options.onMessage(data, container, event);
225
227
  };
226
228
  window.addEventListener("message", this.messageHandler);
229
+ return {
230
+ container,
231
+ iframe: this.iframe
232
+ };
227
233
  }
228
234
  close() {
229
235
  if (typeof window === "undefined") return;
@@ -389,6 +395,7 @@ var LOGIN_CALLBACK_RESULT_PARAM = "yd_login_result";
389
395
  var LOGIN_CALLBACK_RETURN_HASH_PARAM = "yd_login_return_hash";
390
396
  var LOGIN_CALLBACK_STATE_PARAM = "yd_login_state";
391
397
  var LOGIN_CALLBACK_STORAGE_PREFIX = "yd-login-callback:";
398
+ var DEFAULT_IFRAME_LOAD_TIMEOUT_MS = 6e3;
392
399
  function decodeCallbackPayload(value) {
393
400
  try {
394
401
  const padded = value.padEnd(
@@ -516,6 +523,138 @@ function logLoginWarn(message, details) {
516
523
  if (typeof console === "undefined") return;
517
524
  console.warn(LOGIN_UI_LOG_PREFIX, message, details || {});
518
525
  }
526
+ function getIframeLoadTimeoutMs(options) {
527
+ const timeout = options?.iframeLoadTimeoutMs;
528
+ if (typeof timeout !== "number" || !Number.isFinite(timeout)) {
529
+ return DEFAULT_IFRAME_LOAD_TIMEOUT_MS;
530
+ }
531
+ return Math.max(0, timeout);
532
+ }
533
+ function applyLoginPanelStyle(panel) {
534
+ Object.assign(panel.style, {
535
+ position: "absolute",
536
+ inset: "0",
537
+ display: "flex",
538
+ alignItems: "center",
539
+ justifyContent: "center",
540
+ border: "1px solid rgba(229,229,229,0.92)",
541
+ borderRadius: "28px",
542
+ background: "rgba(255,255,255,0.94)",
543
+ color: "#0f172a",
544
+ padding: "24px",
545
+ textAlign: "center",
546
+ zIndex: "1",
547
+ boxShadow: "0 18px 50px rgba(15,15,15,0.06)",
548
+ backdropFilter: "blur(16px)"
549
+ });
550
+ }
551
+ function createLoginPanelContent() {
552
+ const content = document.createElement("div");
553
+ Object.assign(content.style, {
554
+ maxWidth: "360px"
555
+ });
556
+ const badge = document.createElement("div");
557
+ badge.textContent = "YOUiDIAN";
558
+ Object.assign(badge.style, {
559
+ display: "inline-flex",
560
+ alignItems: "center",
561
+ border: "1px solid rgba(229,229,229,0.92)",
562
+ borderRadius: "9999px",
563
+ background: "#fafafa",
564
+ color: "#171717",
565
+ fontSize: "11px",
566
+ fontWeight: "700",
567
+ letterSpacing: "0.22em",
568
+ lineHeight: "1",
569
+ marginBottom: "18px",
570
+ padding: "8px 12px"
571
+ });
572
+ content.appendChild(badge);
573
+ return content;
574
+ }
575
+ function createLoginPanelTitle(value) {
576
+ const title = document.createElement("div");
577
+ title.textContent = value;
578
+ Object.assign(title.style, {
579
+ color: "#171717",
580
+ fontSize: "18px",
581
+ fontWeight: "700",
582
+ lineHeight: "1.35",
583
+ marginBottom: "10px"
584
+ });
585
+ return title;
586
+ }
587
+ function createLoginPanelDescription(value) {
588
+ const description = document.createElement("div");
589
+ description.textContent = value;
590
+ Object.assign(description.style, {
591
+ color: "#525252",
592
+ fontSize: "14px",
593
+ lineHeight: "1.6",
594
+ marginBottom: "20px"
595
+ });
596
+ return description;
597
+ }
598
+ function ensureLoginLoadingStyles() {
599
+ if (document.getElementById("youidian-login-loading-style")) {
600
+ return;
601
+ }
602
+ const style = document.createElement("style");
603
+ style.id = "youidian-login-loading-style";
604
+ style.textContent = "@keyframes youidian-login-spin{to{transform:rotate(360deg)}}";
605
+ document.head.appendChild(style);
606
+ }
607
+ function createLoginLoadingPanel(options) {
608
+ ensureLoginLoadingStyles();
609
+ const panel = document.createElement("div");
610
+ applyLoginPanelStyle(panel);
611
+ const content = createLoginPanelContent();
612
+ const spinner = document.createElement("div");
613
+ Object.assign(spinner.style, {
614
+ width: "22px",
615
+ height: "22px",
616
+ borderRadius: "9999px",
617
+ border: "2px solid rgba(23,23,23,0.16)",
618
+ borderTopColor: "#171717",
619
+ margin: "0 auto 18px",
620
+ animation: "youidian-login-spin 780ms linear infinite"
621
+ });
622
+ content.appendChild(spinner);
623
+ content.appendChild(createLoginPanelTitle(options.title));
624
+ content.appendChild(createLoginPanelDescription(options.description));
625
+ panel.appendChild(content);
626
+ return panel;
627
+ }
628
+ function createLoginRedirectingPanel(options) {
629
+ return createLoginLoadingPanel(options);
630
+ }
631
+ function createLoginFallbackPanel(options) {
632
+ const panel = document.createElement("div");
633
+ applyLoginPanelStyle(panel);
634
+ const content = createLoginPanelContent();
635
+ const button = document.createElement("button");
636
+ button.type = "button";
637
+ button.textContent = options.buttonText;
638
+ Object.assign(button.style, {
639
+ width: "100%",
640
+ border: "0",
641
+ borderRadius: "12px",
642
+ background: "#0a0a0a",
643
+ color: "#ffffff",
644
+ cursor: "pointer",
645
+ fontSize: "14px",
646
+ fontWeight: "700",
647
+ minHeight: "44px",
648
+ padding: "12px 16px",
649
+ boxShadow: "0 12px 28px rgba(17,17,17,0.14)"
650
+ });
651
+ button.onclick = options.onRedirect;
652
+ content.appendChild(createLoginPanelTitle(options.title));
653
+ content.appendChild(createLoginPanelDescription(options.description));
654
+ content.appendChild(button);
655
+ panel.appendChild(content);
656
+ return panel;
657
+ }
519
658
  var LoginUI = class extends HostedFrameModal {
520
659
  constructor() {
521
660
  super(...arguments);
@@ -524,6 +663,11 @@ var LoginUI = class extends HostedFrameModal {
524
663
  __publicField(this, "callbackStorageHandler", null);
525
664
  __publicField(this, "popupMessageHandler", null);
526
665
  __publicField(this, "closeMonitor", null);
666
+ __publicField(this, "iframeFallbackPanel", null);
667
+ __publicField(this, "iframeLoadingPanel", null);
668
+ __publicField(this, "iframeLoadTimer", null);
669
+ __publicField(this, "iframeRedirectTimer", null);
670
+ __publicField(this, "iframeReady", false);
527
671
  __publicField(this, "completed", false);
528
672
  }
529
673
  cleanup() {
@@ -541,11 +685,114 @@ var LoginUI = class extends HostedFrameModal {
541
685
  if (typeof window !== "undefined" && this.closeMonitor) {
542
686
  window.clearInterval(this.closeMonitor);
543
687
  }
688
+ if (typeof window !== "undefined" && this.iframeLoadTimer) {
689
+ window.clearTimeout(this.iframeLoadTimer);
690
+ }
691
+ if (typeof window !== "undefined" && this.iframeRedirectTimer) {
692
+ window.clearTimeout(this.iframeRedirectTimer);
693
+ }
694
+ if (this.iframeFallbackPanel?.parentNode) {
695
+ this.iframeFallbackPanel.parentNode.removeChild(this.iframeFallbackPanel);
696
+ }
697
+ if (this.iframeLoadingPanel?.parentNode) {
698
+ this.iframeLoadingPanel.parentNode.removeChild(this.iframeLoadingPanel);
699
+ }
544
700
  this.popupMessageHandler = null;
545
701
  this.closeMonitor = null;
702
+ this.iframeFallbackPanel = null;
703
+ this.iframeLoadingPanel = null;
704
+ this.iframeLoadTimer = null;
705
+ this.iframeRedirectTimer = null;
706
+ this.iframeReady = false;
546
707
  this.popup = null;
547
708
  this.completed = false;
548
709
  }
710
+ markIframeReady() {
711
+ this.iframeReady = true;
712
+ if (typeof window !== "undefined" && this.iframeLoadTimer) {
713
+ window.clearTimeout(this.iframeLoadTimer);
714
+ this.iframeLoadTimer = null;
715
+ }
716
+ if (typeof window !== "undefined" && this.iframeRedirectTimer) {
717
+ window.clearTimeout(this.iframeRedirectTimer);
718
+ this.iframeRedirectTimer = null;
719
+ }
720
+ if (this.iframeFallbackPanel?.parentNode) {
721
+ this.iframeFallbackPanel.parentNode.removeChild(this.iframeFallbackPanel);
722
+ }
723
+ if (this.iframeLoadingPanel?.parentNode) {
724
+ this.iframeLoadingPanel.parentNode.removeChild(this.iframeLoadingPanel);
725
+ }
726
+ this.iframeFallbackPanel = null;
727
+ this.iframeLoadingPanel = null;
728
+ }
729
+ showIframeLoading(container, options) {
730
+ if (this.iframeLoadingPanel || this.iframeReady) {
731
+ return;
732
+ }
733
+ const panel = createLoginLoadingPanel({
734
+ description: options?.loadingDescription || "Please wait while the secure login page opens.",
735
+ title: options?.loadingTitle || "Opening login page"
736
+ });
737
+ this.iframeLoadingPanel = panel;
738
+ container.appendChild(panel);
739
+ }
740
+ showIframeFallback(options) {
741
+ if (this.iframeReady || this.iframeFallbackPanel) {
742
+ return;
743
+ }
744
+ const { container, finalUrl, launchPayload, loginOptions } = options;
745
+ logLoginWarn("Hosted login iframe did not report ready in time", {
746
+ loginUrl: finalUrl
747
+ });
748
+ if (this.iframeLoadingPanel?.parentNode) {
749
+ this.iframeLoadingPanel.parentNode.removeChild(this.iframeLoadingPanel);
750
+ }
751
+ this.iframeLoadingPanel = null;
752
+ if (loginOptions?.fallbackRedirectMode !== "manual") {
753
+ const panel2 = createLoginRedirectingPanel({
754
+ description: loginOptions?.fallbackDescription || "The embedded login page is taking longer than expected. Redirecting you to the login page now.",
755
+ title: loginOptions?.fallbackTitle || "Redirecting to login page"
756
+ });
757
+ this.iframeFallbackPanel = panel2;
758
+ container.appendChild(panel2);
759
+ loginOptions?.onFallbackVisible?.();
760
+ this.iframeRedirectTimer = window.setTimeout(() => {
761
+ this.iframeRedirectTimer = null;
762
+ logLoginInfo("Redirecting to hosted login fallback page", {
763
+ loginUrl: finalUrl
764
+ });
765
+ window.location.assign(finalUrl);
766
+ }, 350);
767
+ return;
768
+ }
769
+ const panel = createLoginFallbackPanel({
770
+ buttonText: loginOptions?.fallbackButtonText || "Continue to login",
771
+ description: loginOptions?.fallbackDescription || "The embedded login page is taking longer than expected. Continue on the login page to finish signing in.",
772
+ title: loginOptions?.fallbackTitle || "Login page is still loading",
773
+ onRedirect: () => {
774
+ logLoginInfo("Redirecting to hosted login fallback page", {
775
+ loginUrl: finalUrl
776
+ });
777
+ window.location.assign(finalUrl);
778
+ }
779
+ });
780
+ this.iframeFallbackPanel = panel;
781
+ container.appendChild(panel);
782
+ loginOptions?.onFallbackVisible?.();
783
+ }
784
+ startIframeWatchdog(options) {
785
+ if (typeof window === "undefined") return;
786
+ const timeoutMs = getIframeLoadTimeoutMs(options.loginOptions);
787
+ if (timeoutMs === 0) {
788
+ this.showIframeFallback(options);
789
+ return;
790
+ }
791
+ this.iframeLoadTimer = window.setTimeout(() => {
792
+ this.iframeLoadTimer = null;
793
+ this.showIframeFallback(options);
794
+ }, timeoutMs);
795
+ }
549
796
  handleLoginEvent(data, options) {
550
797
  if (!data || typeof data !== "object" || !data.type) {
551
798
  logLoginDebug("Ignored non-login event payload");
@@ -555,6 +802,8 @@ var LoginUI = class extends HostedFrameModal {
555
802
  type: data.type
556
803
  });
557
804
  switch (data.type) {
805
+ case "LOGIN_READY":
806
+ break;
558
807
  case "LOGIN_SUCCESS":
559
808
  if (this.completed) {
560
809
  break;
@@ -662,15 +911,20 @@ var LoginUI = class extends HostedFrameModal {
662
911
  parentOrigin: launchPayload.origin || null
663
912
  });
664
913
  this.completed = false;
914
+ this.iframeReady = false;
665
915
  this.listenForLoginCallback(callbackState, options);
666
- this.openHostedFrame(finalUrl, {
916
+ const hostedFrame = this.openHostedFrame(finalUrl, {
667
917
  allowedOrigin: options?.allowedOrigin || loginOrigin || void 0,
668
918
  height: "min(760px, 94vh)",
669
919
  onCloseButton: () => {
670
920
  options?.onCancel?.();
671
921
  options?.onClose?.();
672
922
  },
673
- onMessage: (data, container) => {
923
+ onMessage: (data, container, event) => {
924
+ const isIframeEvent = event.source === this.iframe?.contentWindow;
925
+ if (isIframeEvent && (data.type === "LOGIN_READY" || data.type === "LOGIN_RESIZE") || data.type === "LOGIN_SUCCESS" || data.type === "LOGIN_ERROR") {
926
+ this.markIframeReady();
927
+ }
674
928
  if (data.type === "LOGIN_RESIZE" && data.height) {
675
929
  const maxHeight = window.innerHeight * 0.94;
676
930
  container.style.height = `${Math.min(data.height, maxHeight)}px`;
@@ -679,6 +933,15 @@ var LoginUI = class extends HostedFrameModal {
679
933
  },
680
934
  width: "min(520px, 100%)"
681
935
  });
936
+ if (hostedFrame) {
937
+ this.showIframeLoading(hostedFrame.container, options);
938
+ this.startIframeWatchdog({
939
+ container: hostedFrame.container,
940
+ finalUrl,
941
+ launchPayload,
942
+ loginOptions: options
943
+ });
944
+ }
682
945
  }
683
946
  openLoginPopup(params, options) {
684
947
  if (typeof window === "undefined") return;
@@ -808,14 +1071,34 @@ var PaymentUI = class extends HostedFrameModal {
808
1071
  priceId,
809
1072
  productCode,
810
1073
  userId,
1074
+ customAmount,
811
1075
  checkoutUrl: checkoutUrlParam,
812
1076
  baseUrl = "https://pay.imgto.link"
813
1077
  } = urlOrParams;
814
1078
  const base = (checkoutUrlParam || baseUrl).replace(/\/$/, "");
1079
+ const query = new URLSearchParams({
1080
+ userId
1081
+ });
1082
+ if (customAmount) {
1083
+ const amount = Number(customAmount.amount);
1084
+ const currency = customAmount.currency.trim().toUpperCase();
1085
+ if (!Number.isInteger(amount) || amount <= 0) {
1086
+ throw new Error(
1087
+ "customAmount.amount must be a positive integer in the smallest currency unit"
1088
+ );
1089
+ }
1090
+ if (!/^[A-Z]{3}$/.test(currency)) {
1091
+ throw new Error(
1092
+ "customAmount.currency must be a 3-letter currency code"
1093
+ );
1094
+ }
1095
+ query.set("customAmount", String(amount));
1096
+ query.set("customCurrency", currency);
1097
+ }
815
1098
  if (productCode) {
816
- checkoutUrl = `${base}/checkout/${appId}/code/${productCode}?userId=${encodeURIComponent(userId)}`;
1099
+ checkoutUrl = `${base}/checkout/${appId}/code/${productCode}?${query.toString()}`;
817
1100
  } else if (productId && priceId) {
818
- checkoutUrl = `${base}/checkout/${appId}/${productId}/${priceId}?userId=${encodeURIComponent(userId)}`;
1101
+ checkoutUrl = `${base}/checkout/${appId}/${productId}/${priceId}?${query.toString()}`;
819
1102
  } else {
820
1103
  throw new Error(
821
1104
  "Either productCode or both productId and priceId are required"
@@ -902,6 +1185,69 @@ function createPaymentUI() {
902
1185
 
903
1186
  // src/server.ts
904
1187
  var import_crypto = __toESM(require("crypto"), 1);
1188
+ function getCustomAmountRechargeRule(product, currency) {
1189
+ if (!product || product.type !== "CREDIT") return null;
1190
+ const normalizedCurrency = currency.trim().toUpperCase();
1191
+ if (!/^[A-Z]{3}$/.test(normalizedCurrency)) return null;
1192
+ const customAmount = product.metadata?.customAmount;
1193
+ if (customAmount?.enabled !== true) return null;
1194
+ const currencyConfig = customAmount.currencies?.[normalizedCurrency];
1195
+ if (!currencyConfig) return null;
1196
+ const minimumGrantAmount = currencyConfig.unitsPerCurrencyUnitBasis === "MAJOR" ? Math.ceil(50 / currencyConfig.unitsPerCurrencyUnit) : Math.ceil(1 / currencyConfig.unitsPerCurrencyUnit);
1197
+ let minAmount = Math.max(currencyConfig.minAmount, minimumGrantAmount);
1198
+ if (currencyConfig.stepAmount && minAmount > currencyConfig.minAmount) {
1199
+ const stepCount = Math.ceil(
1200
+ (minAmount - currencyConfig.minAmount) / currencyConfig.stepAmount
1201
+ );
1202
+ minAmount = currencyConfig.minAmount + stepCount * currencyConfig.stepAmount;
1203
+ }
1204
+ return {
1205
+ ...currencyConfig,
1206
+ productId: product.id,
1207
+ productCode: product.code,
1208
+ entitlementKey: customAmount.entitlementKey,
1209
+ currency: normalizedCurrency,
1210
+ configuredMinAmount: currencyConfig.minAmount,
1211
+ minimumGrantAmount,
1212
+ minAmount
1213
+ };
1214
+ }
1215
+ function validateCustomAmountRecharge(product, customAmount) {
1216
+ const rule = getCustomAmountRechargeRule(product, customAmount.currency);
1217
+ if (!rule) {
1218
+ return {
1219
+ valid: false,
1220
+ code: "CUSTOM_AMOUNT_UNAVAILABLE",
1221
+ error: "Product does not support custom amount recharge for this currency."
1222
+ };
1223
+ }
1224
+ const amount = Number(customAmount.amount);
1225
+ if (!Number.isInteger(amount) || amount <= 0) {
1226
+ return {
1227
+ valid: false,
1228
+ code: "CUSTOM_AMOUNT_INVALID_AMOUNT",
1229
+ error: "Custom amount must be a positive integer in the smallest currency unit.",
1230
+ rule
1231
+ };
1232
+ }
1233
+ if (amount < rule.minAmount || amount > rule.maxAmount) {
1234
+ return {
1235
+ valid: false,
1236
+ code: "CUSTOM_AMOUNT_OUT_OF_RANGE",
1237
+ error: `Custom amount must be between ${rule.minAmount} and ${rule.maxAmount}.`,
1238
+ rule
1239
+ };
1240
+ }
1241
+ if (rule.stepAmount && (amount - rule.configuredMinAmount) % rule.stepAmount !== 0) {
1242
+ return {
1243
+ valid: false,
1244
+ code: "CUSTOM_AMOUNT_INVALID_STEP",
1245
+ error: `Custom amount must follow step ${rule.stepAmount}.`,
1246
+ rule
1247
+ };
1248
+ }
1249
+ return { valid: true, rule };
1250
+ }
905
1251
  var PaymentClient = class {
906
1252
  // 用于生成 checkout URL
907
1253
  constructor(options) {
@@ -1194,6 +1540,8 @@ var PaymentClient = class {
1194
1540
  PaymentUI,
1195
1541
  createLoginUI,
1196
1542
  createPaymentUI,
1197
- handleLoginCallbackIfPresent
1543
+ getCustomAmountRechargeRule,
1544
+ handleLoginCallbackIfPresent,
1545
+ validateCustomAmountRecharge
1198
1546
  });
1199
1547
  //# sourceMappingURL=index.cjs.map