@youidian/sdk 3.3.7 → 3.4.1

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.
@@ -21,8 +21,8 @@ declare class HostedFrameModal<TData extends HostedFrameMessage> {
21
21
  close(): void;
22
22
  }
23
23
 
24
- type LoginEventType = "LOGIN_READY" | "LOGIN_SUCCESS" | "LOGIN_CANCELLED" | "LOGIN_CLOSE" | "LOGIN_RESIZE" | "LOGIN_ERROR";
25
- type LoginDisplayMode = "modal" | "popup";
24
+ type LoginEventType = "LOGIN_READY" | "LOGIN_SUCCESS" | "LOGIN_CANCELLED" | "LOGIN_CLOSE" | "LOGIN_RESIZE" | "LOGIN_REDIRECT_REQUIRED" | "LOGIN_ERROR";
25
+ type LoginDisplayMode = "auto" | "modal" | "popup" | "redirect";
26
26
  type LoginFallbackRedirectMode = "auto" | "manual";
27
27
  interface LoginResult {
28
28
  avatar?: string | null;
@@ -43,6 +43,9 @@ interface LoginResult {
43
43
  }
44
44
  interface LoginEventData extends HostedFrameMessage, LoginResult {
45
45
  type: LoginEventType;
46
+ attemptId?: string;
47
+ authUrl?: string;
48
+ channel?: string;
46
49
  error?: string;
47
50
  message?: string;
48
51
  }
@@ -54,8 +57,8 @@ interface LoginUIOptions {
54
57
  /** Optional same-origin URL used as the no-API OAuth callback landing page. */
55
58
  callbackUrl?: string;
56
59
  /**
57
- * How to open the hosted login page. Defaults to "modal", which embeds the
58
- * login page in an iframe modal. Use "popup" to keep the legacy popup flow.
60
+ * How to open the hosted login page. Defaults to "auto", which uses an
61
+ * iframe modal on desktop and redirects on mobile/embedded browsers.
59
62
  */
60
63
  displayMode?: LoginDisplayMode;
61
64
  /** Text for the fallback button shown when the iframe does not report readiness. */
@@ -142,6 +145,7 @@ declare class LoginUI extends HostedFrameModal<LoginEventData> {
142
145
  private showIframeFallback;
143
146
  private startIframeWatchdog;
144
147
  private handleLoginEvent;
148
+ private openLoginRedirect;
145
149
  private listenForLoginCallback;
146
150
  close(): void;
147
151
  private buildOpenContext;
@@ -21,8 +21,8 @@ declare class HostedFrameModal<TData extends HostedFrameMessage> {
21
21
  close(): void;
22
22
  }
23
23
 
24
- type LoginEventType = "LOGIN_READY" | "LOGIN_SUCCESS" | "LOGIN_CANCELLED" | "LOGIN_CLOSE" | "LOGIN_RESIZE" | "LOGIN_ERROR";
25
- type LoginDisplayMode = "modal" | "popup";
24
+ type LoginEventType = "LOGIN_READY" | "LOGIN_SUCCESS" | "LOGIN_CANCELLED" | "LOGIN_CLOSE" | "LOGIN_RESIZE" | "LOGIN_REDIRECT_REQUIRED" | "LOGIN_ERROR";
25
+ type LoginDisplayMode = "auto" | "modal" | "popup" | "redirect";
26
26
  type LoginFallbackRedirectMode = "auto" | "manual";
27
27
  interface LoginResult {
28
28
  avatar?: string | null;
@@ -43,6 +43,9 @@ interface LoginResult {
43
43
  }
44
44
  interface LoginEventData extends HostedFrameMessage, LoginResult {
45
45
  type: LoginEventType;
46
+ attemptId?: string;
47
+ authUrl?: string;
48
+ channel?: string;
46
49
  error?: string;
47
50
  message?: string;
48
51
  }
@@ -54,8 +57,8 @@ interface LoginUIOptions {
54
57
  /** Optional same-origin URL used as the no-API OAuth callback landing page. */
55
58
  callbackUrl?: string;
56
59
  /**
57
- * How to open the hosted login page. Defaults to "modal", which embeds the
58
- * login page in an iframe modal. Use "popup" to keep the legacy popup flow.
60
+ * How to open the hosted login page. Defaults to "auto", which uses an
61
+ * iframe modal on desktop and redirects on mobile/embedded browsers.
59
62
  */
60
63
  displayMode?: LoginDisplayMode;
61
64
  /** Text for the fallback button shown when the iframe does not report readiness. */
@@ -142,6 +145,7 @@ declare class LoginUI extends HostedFrameModal<LoginEventData> {
142
145
  private showIframeFallback;
143
146
  private startIframeWatchdog;
144
147
  private handleLoginEvent;
148
+ private openLoginRedirect;
145
149
  private listenForLoginCallback;
146
150
  close(): void;
147
151
  private buildOpenContext;
package/dist/login.cjs CHANGED
@@ -28,6 +28,91 @@ __export(login_exports, {
28
28
  });
29
29
  module.exports = __toCommonJS(login_exports);
30
30
 
31
+ // src/environment.ts
32
+ function normalizeText(value) {
33
+ return value || "";
34
+ }
35
+ function getRuntimeInput() {
36
+ if (typeof navigator === "undefined") {
37
+ return {};
38
+ }
39
+ const nav = navigator;
40
+ const coarsePointer = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(pointer: coarse)").matches : void 0;
41
+ return {
42
+ coarsePointer,
43
+ maxTouchPoints: nav.maxTouchPoints || 0,
44
+ platform: nav.platform || "",
45
+ standalone: nav.standalone,
46
+ userAgent: nav.userAgent || "",
47
+ userAgentDataMobile: nav.userAgentData?.mobile
48
+ };
49
+ }
50
+ function includesAny(value, patterns) {
51
+ return patterns.some((pattern) => pattern.test(value));
52
+ }
53
+ function detectLoginEnvironment(input = getRuntimeInput()) {
54
+ const userAgent = normalizeText(input.userAgent);
55
+ const platform = normalizeText(input.platform);
56
+ const rawMaxTouchPoints = input.maxTouchPoints ?? 0;
57
+ const maxTouchPoints = Number.isFinite(rawMaxTouchPoints) ? Math.max(0, rawMaxTouchPoints) : 0;
58
+ const isCoarsePointer = input.coarsePointer === true;
59
+ const isTouch = maxTouchPoints > 0 || isCoarsePointer;
60
+ const isWeChat = /micromessenger/i.test(userAgent);
61
+ const isAndroid = /\bandroid\b/i.test(userAgent);
62
+ const hasExplicitIOSDevice = /\b(iPad|iPhone|iPod)\b/i.test(userAgent);
63
+ const hasMacintoshUserAgent = /\bMacintosh\b/i.test(userAgent);
64
+ const hasMacPlatform = /^Mac/i.test(platform);
65
+ const isIPadOS = !hasExplicitIOSDevice && hasMacintoshUserAgent && hasMacPlatform && typeof input.standalone !== "undefined" && maxTouchPoints > 2;
66
+ const isIOS = hasExplicitIOSDevice || isIPadOS;
67
+ const isTabletByUa = includesAny(userAgent, [
68
+ /\biPad\b/i,
69
+ /\btablet\b/i,
70
+ /\bplaybook\b/i,
71
+ /\bsilk\b(?!.*\bmobile\b)/i,
72
+ /\bkindle\b/i,
73
+ /\bnexus 7\b/i,
74
+ /\bnexus 9\b/i,
75
+ /\bxoom\b/i,
76
+ /\bsm-t\d+/i,
77
+ /\bgt-p\d+/i,
78
+ /\bmi pad\b/i
79
+ ]);
80
+ const isMobileByUa = includesAny(userAgent, [
81
+ /\bMobile\b/i,
82
+ /\biPhone\b/i,
83
+ /\biPod\b/i,
84
+ /\bWindows Phone\b/i,
85
+ /\bIEMobile\b/i,
86
+ /\bBlackBerry\b/i,
87
+ /\bBB10\b/i,
88
+ /\bOpera Mini\b/i,
89
+ /\bOpera Mobi\b/i,
90
+ /\bwebOS\b/i
91
+ ]);
92
+ const isAndroidTablet = isAndroid && !/\bMobile\b/i.test(userAgent);
93
+ const isTablet = isIPadOS || isTabletByUa || isAndroidTablet;
94
+ const isMobile = input.userAgentDataMobile === true || isMobileByUa || isAndroid && !isTablet || isIOS && !isTablet;
95
+ const deviceType = isTablet ? "tablet" : isMobile ? "mobile" : userAgent || platform || isTouch ? "desktop" : "unknown";
96
+ const shouldUseRedirectLogin = deviceType === "mobile" || deviceType === "tablet" || isWeChat;
97
+ return {
98
+ deviceType,
99
+ isAndroid,
100
+ isCoarsePointer,
101
+ isDesktop: deviceType === "desktop",
102
+ isIOS,
103
+ isIPadOS,
104
+ isMobile,
105
+ isStandalone: input.standalone === true,
106
+ isTablet,
107
+ isTouch,
108
+ isWeChat,
109
+ maxTouchPoints,
110
+ platform,
111
+ shouldUseRedirectLogin,
112
+ userAgent
113
+ };
114
+ }
115
+
31
116
  // src/hosted-modal.ts
32
117
  var SDK_SUPPORTED_LOCALES = [
33
118
  "en",
@@ -239,6 +324,15 @@ function getOrigin(value) {
239
324
  return null;
240
325
  }
241
326
  }
327
+ function isValidLoginRedirectUrl(value) {
328
+ if (!value) return false;
329
+ try {
330
+ const url = new URL(value);
331
+ return url.protocol === "https:" || url.protocol === "http:";
332
+ } catch {
333
+ return false;
334
+ }
335
+ }
242
336
  function createLoginCallbackState() {
243
337
  if (typeof crypto !== "undefined" && crypto.randomUUID) {
244
338
  return crypto.randomUUID().replace(/-/g, "");
@@ -585,6 +679,24 @@ function applyLoginPanelStyle(panel) {
585
679
  backdropFilter: "blur(16px)"
586
680
  });
587
681
  }
682
+ function applyLoginLoadingPanelStyle(panel) {
683
+ Object.assign(panel.style, {
684
+ position: "absolute",
685
+ inset: "0",
686
+ display: "flex",
687
+ alignItems: "center",
688
+ justifyContent: "center",
689
+ border: "0",
690
+ borderRadius: "28px",
691
+ background: "rgba(255,255,255,0.94)",
692
+ color: "#0f172a",
693
+ padding: "24px",
694
+ textAlign: "center",
695
+ zIndex: "1",
696
+ boxShadow: "none",
697
+ backdropFilter: "blur(16px)"
698
+ });
699
+ }
588
700
  function createLoginPanelContent() {
589
701
  const content = document.createElement("div");
590
702
  Object.assign(content.style, {
@@ -609,6 +721,71 @@ function createLoginPanelContent() {
609
721
  content.appendChild(badge);
610
722
  return content;
611
723
  }
724
+ function createBrandLoadingMark() {
725
+ const logo = document.createElementNS("http://www.w3.org/2000/svg", "svg");
726
+ logo.setAttribute("viewBox", "0 0 1024 1024");
727
+ logo.setAttribute("fill", "none");
728
+ logo.setAttribute("aria-hidden", "true");
729
+ Object.assign(logo.style, {
730
+ width: "112px",
731
+ height: "112px",
732
+ color: "#22c55e",
733
+ display: "block",
734
+ overflow: "visible"
735
+ });
736
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
737
+ group.setAttribute("fill", "currentColor");
738
+ const ring = document.createElementNS("http://www.w3.org/2000/svg", "path");
739
+ ring.setAttribute(
740
+ "d",
741
+ "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"
742
+ );
743
+ ring.setAttribute("class", "youidian-sdk-brand-loading__ring");
744
+ const dot = document.createElementNS("http://www.w3.org/2000/svg", "circle");
745
+ dot.setAttribute("class", "youidian-sdk-brand-loading__dot");
746
+ dot.setAttribute("cx", "714.5");
747
+ dot.setAttribute("cy", "490.5");
748
+ dot.setAttribute("r", "68");
749
+ group.appendChild(ring);
750
+ group.appendChild(dot);
751
+ logo.appendChild(group);
752
+ return logo;
753
+ }
754
+ function createBrandLoadingCopy() {
755
+ const brand = document.createElement("div");
756
+ brand.className = "youidian-sdk-brand-loading__copy";
757
+ Object.assign(brand.style, {
758
+ display: "flex",
759
+ flexDirection: "column",
760
+ alignItems: "flex-start",
761
+ justifyContent: "center",
762
+ lineHeight: "1"
763
+ });
764
+ const name = document.createElement("div");
765
+ name.className = "youidian-sdk-brand-loading__name";
766
+ name.textContent = "\u4F18\u6613\u70B9";
767
+ Object.assign(name.style, {
768
+ color: "#0f172a",
769
+ fontSize: "34px",
770
+ fontWeight: "900",
771
+ letterSpacing: "0",
772
+ lineHeight: "1"
773
+ });
774
+ const tagline = document.createElement("div");
775
+ tagline.className = "youidian-sdk-brand-loading__tagline";
776
+ tagline.textContent = "\u5F00\u53D1\u8005\u670D\u52A1\u4E2D\u53F0";
777
+ Object.assign(tagline.style, {
778
+ color: "#64748b",
779
+ fontSize: "16px",
780
+ fontWeight: "700",
781
+ letterSpacing: "0.24em",
782
+ lineHeight: "1",
783
+ marginTop: "6px"
784
+ });
785
+ brand.appendChild(name);
786
+ brand.appendChild(tagline);
787
+ return brand;
788
+ }
612
789
  function createLoginPanelTitle(value) {
613
790
  const title = document.createElement("div");
614
791
  title.textContent = value;
@@ -638,32 +815,111 @@ function ensureLoginLoadingStyles() {
638
815
  }
639
816
  const style = document.createElement("style");
640
817
  style.id = "youidian-login-loading-style";
641
- style.textContent = "@keyframes youidian-login-spin{to{transform:rotate(360deg)}}";
818
+ style.textContent = `
819
+ .youidian-sdk-brand-loading__ring,
820
+ .youidian-sdk-brand-loading__dot {
821
+ transform-box: fill-box;
822
+ transform-origin: center;
823
+ will-change: opacity, transform;
824
+ }
825
+
826
+ .youidian-sdk-brand-loading__ring {
827
+ animation: youidian-sdk-brand-loading-ring 1400ms cubic-bezier(0.4, 0, 0.2, 1) infinite;
828
+ }
829
+
830
+ .youidian-sdk-brand-loading__dot {
831
+ animation: youidian-sdk-brand-loading-dot 1400ms cubic-bezier(0.4, 0, 0.2, 1) infinite;
832
+ }
833
+
834
+ .youidian-sdk-brand-loading__copy {
835
+ will-change: opacity, transform;
836
+ animation: youidian-sdk-brand-loading-copy 1600ms cubic-bezier(0.4, 0, 0.2, 1) infinite;
837
+ }
838
+
839
+ .youidian-sdk-brand-loading__tagline {
840
+ will-change: opacity;
841
+ animation: youidian-sdk-brand-loading-tagline 1600ms cubic-bezier(0.4, 0, 0.2, 1) infinite;
842
+ }
843
+
844
+ @keyframes youidian-sdk-brand-loading-ring {
845
+ 0%,
846
+ 100% {
847
+ opacity: 0.72;
848
+ transform: rotate(0deg) scale(0.98);
849
+ }
850
+ 50% {
851
+ opacity: 1;
852
+ transform: rotate(8deg) scale(1.02);
853
+ }
854
+ }
855
+
856
+ @keyframes youidian-sdk-brand-loading-dot {
857
+ 0%,
858
+ 100% {
859
+ opacity: 0.55;
860
+ transform: scale(0.9);
861
+ }
862
+ 50% {
863
+ opacity: 1;
864
+ transform: scale(1.08);
865
+ }
866
+ }
867
+
868
+ @keyframes youidian-sdk-brand-loading-copy {
869
+ 0%,
870
+ 100% {
871
+ opacity: 0.86;
872
+ transform: translateY(0);
873
+ }
874
+ 50% {
875
+ opacity: 1;
876
+ transform: translateY(-1px);
877
+ }
878
+ }
879
+
880
+ @keyframes youidian-sdk-brand-loading-tagline {
881
+ 0%,
882
+ 100% {
883
+ opacity: 0.64;
884
+ }
885
+ 50% {
886
+ opacity: 0.92;
887
+ }
888
+ }
889
+
890
+ @media (prefers-reduced-motion: reduce) {
891
+ .youidian-sdk-brand-loading__ring,
892
+ .youidian-sdk-brand-loading__dot,
893
+ .youidian-sdk-brand-loading__copy,
894
+ .youidian-sdk-brand-loading__tagline {
895
+ animation: none;
896
+ opacity: 1;
897
+ transform: none;
898
+ }
899
+ }
900
+ `;
642
901
  document.head.appendChild(style);
643
902
  }
644
- function createLoginLoadingPanel(options) {
903
+ function createLoginLoadingPanel() {
645
904
  ensureLoginLoadingStyles();
646
905
  const panel = document.createElement("div");
647
- applyLoginPanelStyle(panel);
648
- const content = createLoginPanelContent();
649
- const spinner = document.createElement("div");
650
- Object.assign(spinner.style, {
651
- width: "22px",
652
- height: "22px",
653
- borderRadius: "9999px",
654
- border: "2px solid rgba(23,23,23,0.16)",
655
- borderTopColor: "#171717",
656
- margin: "0 auto 18px",
657
- animation: "youidian-login-spin 780ms linear infinite"
906
+ applyLoginLoadingPanelStyle(panel);
907
+ panel.setAttribute("role", "status");
908
+ panel.setAttribute("aria-label", "\u4F18\u6613\u70B9\u5F00\u53D1\u8005\u670D\u52A1\u4E2D\u53F0");
909
+ const content = document.createElement("div");
910
+ Object.assign(content.style, {
911
+ display: "flex",
912
+ alignItems: "center",
913
+ justifyContent: "center",
914
+ gap: "12px"
658
915
  });
659
- content.appendChild(spinner);
660
- content.appendChild(createLoginPanelTitle(options.title));
661
- content.appendChild(createLoginPanelDescription(options.description));
916
+ content.appendChild(createBrandLoadingMark());
917
+ content.appendChild(createBrandLoadingCopy());
662
918
  panel.appendChild(content);
663
919
  return panel;
664
920
  }
665
- function createLoginRedirectingPanel(options) {
666
- return createLoginLoadingPanel(options);
921
+ function createLoginRedirectingPanel(_options) {
922
+ return createLoginLoadingPanel();
667
923
  }
668
924
  function createLoginFallbackPanel(options) {
669
925
  const panel = document.createElement("div");
@@ -763,14 +1019,11 @@ var LoginUI = class extends HostedFrameModal {
763
1019
  this.iframeFallbackPanel = null;
764
1020
  this.iframeLoadingPanel = null;
765
1021
  }
766
- showIframeLoading(container, options) {
1022
+ showIframeLoading(container) {
767
1023
  if (this.iframeLoadingPanel || this.iframeReady) {
768
1024
  return;
769
1025
  }
770
- const panel = createLoginLoadingPanel({
771
- description: options?.loadingDescription || "Please wait while the secure login page opens.",
772
- title: options?.loadingTitle || "Opening login page"
773
- });
1026
+ const panel = createLoginLoadingPanel();
774
1027
  this.iframeLoadingPanel = panel;
775
1028
  container.appendChild(panel);
776
1029
  }
@@ -858,6 +1111,20 @@ var LoginUI = class extends HostedFrameModal {
858
1111
  break;
859
1112
  case "LOGIN_RESIZE":
860
1113
  break;
1114
+ case "LOGIN_REDIRECT_REQUIRED":
1115
+ if (!isValidLoginRedirectUrl(data.authUrl)) {
1116
+ logLoginWarn("Login redirect requested with invalid authUrl");
1117
+ options?.onError?.("Login redirect URL is invalid", data);
1118
+ break;
1119
+ }
1120
+ logLoginInfo("Login redirect requested by hosted page", {
1121
+ attemptId: data.attemptId || null,
1122
+ channel: data.channel || null,
1123
+ authUrl: data.authUrl
1124
+ });
1125
+ this.close();
1126
+ window.location.assign(data.authUrl);
1127
+ break;
861
1128
  case "LOGIN_ERROR":
862
1129
  if (this.completed) {
863
1130
  break;
@@ -881,6 +1148,25 @@ var LoginUI = class extends HostedFrameModal {
881
1148
  break;
882
1149
  }
883
1150
  }
1151
+ openLoginRedirect(params, options) {
1152
+ if (typeof window === "undefined") return;
1153
+ const { callbackState, finalUrl, launchPayload } = this.buildOpenContext(
1154
+ params,
1155
+ options
1156
+ );
1157
+ logLoginInfo("Redirecting to hosted login page", {
1158
+ appId: params.appId,
1159
+ callbackState,
1160
+ callbackUrl: launchPayload.callbackUrl || null,
1161
+ hasCallbackUrl: Boolean(launchPayload.callbackUrl),
1162
+ loginUrl: finalUrl,
1163
+ autoClose: options?.autoClose ?? true,
1164
+ displayMode: "redirect",
1165
+ pageUrl: window.location.href,
1166
+ parentOrigin: launchPayload.origin || null
1167
+ });
1168
+ window.location.assign(finalUrl);
1169
+ }
884
1170
  listenForLoginCallback(callbackState, options) {
885
1171
  try {
886
1172
  this.callbackChannel = new BroadcastChannel(
@@ -957,21 +1243,17 @@ var LoginUI = class extends HostedFrameModal {
957
1243
  options?.onCancel?.();
958
1244
  options?.onClose?.();
959
1245
  },
960
- onMessage: (data, container, event) => {
1246
+ onMessage: (data, _container, event) => {
961
1247
  const isIframeEvent = event.source === this.iframe?.contentWindow;
962
1248
  if (isIframeEvent && (data.type === "LOGIN_READY" || data.type === "LOGIN_RESIZE") || data.type === "LOGIN_SUCCESS" || data.type === "LOGIN_ERROR") {
963
1249
  this.markIframeReady();
964
1250
  }
965
- if (data.type === "LOGIN_RESIZE" && data.height) {
966
- const maxHeight = window.innerHeight * 0.94;
967
- container.style.height = `${Math.min(data.height, maxHeight)}px`;
968
- }
969
1251
  this.handleLoginEvent(data, options);
970
1252
  },
971
1253
  width: "min(520px, 100%)"
972
1254
  });
973
1255
  if (hostedFrame) {
974
- this.showIframeLoading(hostedFrame.container, options);
1256
+ this.showIframeLoading(hostedFrame.container);
975
1257
  this.startIframeWatchdog({
976
1258
  container: hostedFrame.container,
977
1259
  finalUrl,
@@ -1075,11 +1357,19 @@ var LoginUI = class extends HostedFrameModal {
1075
1357
  }
1076
1358
  openLogin(params, options) {
1077
1359
  if (typeof window === "undefined") return;
1078
- const displayMode = options?.displayMode ?? "modal";
1360
+ const displayMode = options?.displayMode ?? "auto";
1361
+ if (displayMode === "redirect") {
1362
+ this.openLoginRedirect(params, options);
1363
+ return;
1364
+ }
1079
1365
  if (displayMode === "popup") {
1080
1366
  this.openLoginPopup(params, options);
1081
1367
  return;
1082
1368
  }
1369
+ if (displayMode === "auto" && detectLoginEnvironment().shouldUseRedirectLogin) {
1370
+ this.openLoginRedirect(params, options);
1371
+ return;
1372
+ }
1083
1373
  this.openLoginModal(params, options);
1084
1374
  }
1085
1375
  };