@arcblock/did-connect-service 4.1.61 → 4.1.63

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.
Files changed (72) hide show
  1. package/dist/_generated/asset-bytes.js +1 -1
  2. package/dist/_generated/asset-manifest.js +2 -2
  3. package/dist/assets/{admin-core.bcfa7ce2.js → admin-core.bbd8d53c.js} +963 -194
  4. package/dist/assets/{admin.bd6c819b.css → admin.7b66e78d.css} +160 -7
  5. package/dist/blocklet-service-rpc.js +4 -4
  6. package/dist/blocklet-service-rpc.js.map +1 -1
  7. package/dist/handler-errors.d.ts +14 -8
  8. package/dist/handler-errors.d.ts.map +1 -1
  9. package/dist/handler-errors.js +15 -8
  10. package/dist/handler-errors.js.map +1 -1
  11. package/dist/handlers/account-merge-handler.d.ts +32 -5
  12. package/dist/handlers/account-merge-handler.d.ts.map +1 -1
  13. package/dist/handlers/account-merge-handler.js +155 -19
  14. package/dist/handlers/account-merge-handler.js.map +1 -1
  15. package/dist/handlers/auth-handler.d.ts.map +1 -1
  16. package/dist/handlers/auth-handler.js +11 -6
  17. package/dist/handlers/auth-handler.js.map +1 -1
  18. package/dist/handlers/did-connect-handler.d.ts +2 -2
  19. package/dist/handlers/did-connect-handler.d.ts.map +1 -1
  20. package/dist/handlers/did-connect-handler.js +9 -14
  21. package/dist/handlers/did-connect-handler.js.map +1 -1
  22. package/dist/handlers/email-login-handler.d.ts.map +1 -1
  23. package/dist/handlers/email-login-handler.js +15 -10
  24. package/dist/handlers/email-login-handler.js.map +1 -1
  25. package/dist/handlers/oauth-handler.d.ts +2 -0
  26. package/dist/handlers/oauth-handler.d.ts.map +1 -1
  27. package/dist/handlers/oauth-handler.js +71 -22
  28. package/dist/handlers/oauth-handler.js.map +1 -1
  29. package/dist/handlers/passkey-handler.d.ts +8 -2
  30. package/dist/handlers/passkey-handler.d.ts.map +1 -1
  31. package/dist/handlers/passkey-handler.js +27 -18
  32. package/dist/handlers/passkey-handler.js.map +1 -1
  33. package/dist/handlers/team-handler.js +2 -2
  34. package/dist/handlers/team-handler.js.map +1 -1
  35. package/dist/http/security-headers.d.ts +38 -0
  36. package/dist/http/security-headers.d.ts.map +1 -0
  37. package/dist/http/security-headers.js +67 -0
  38. package/dist/http/security-headers.js.map +1 -0
  39. package/dist/identity/session-epoch.d.ts +11 -0
  40. package/dist/identity/session-epoch.d.ts.map +1 -1
  41. package/dist/identity/session-epoch.js +12 -0
  42. package/dist/identity/session-epoch.js.map +1 -1
  43. package/dist/pages/admin/i18n.d.ts.map +1 -1
  44. package/dist/pages/admin/i18n.js +271 -47
  45. package/dist/pages/admin/i18n.js.map +1 -1
  46. package/dist/pages/admin/merge-confirm-view.d.ts +75 -18
  47. package/dist/pages/admin/merge-confirm-view.d.ts.map +1 -1
  48. package/dist/pages/admin/merge-confirm-view.js +304 -72
  49. package/dist/pages/admin/merge-confirm-view.js.map +1 -1
  50. package/dist/pages/admin/merge-method-picker.d.ts +34 -3
  51. package/dist/pages/admin/merge-method-picker.d.ts.map +1 -1
  52. package/dist/pages/admin/merge-method-picker.js +188 -14
  53. package/dist/pages/admin/merge-method-picker.js.map +1 -1
  54. package/dist/pages/admin/styles.d.ts.map +1 -1
  55. package/dist/pages/admin/styles.js +160 -7
  56. package/dist/pages/admin/styles.js.map +1 -1
  57. package/dist/pages/admin/tab-profile-accounts.d.ts.map +1 -1
  58. package/dist/pages/admin/tab-profile-accounts.js +490 -114
  59. package/dist/pages/admin/tab-profile-accounts.js.map +1 -1
  60. package/dist/pages/oauth-callback-page.d.ts +14 -0
  61. package/dist/pages/oauth-callback-page.d.ts.map +1 -1
  62. package/dist/pages/oauth-callback-page.js +35 -3
  63. package/dist/pages/oauth-callback-page.js.map +1 -1
  64. package/dist/store/account-merge.d.ts +139 -3
  65. package/dist/store/account-merge.d.ts.map +1 -1
  66. package/dist/store/account-merge.js +381 -11
  67. package/dist/store/account-merge.js.map +1 -1
  68. package/dist/store/d1-store.d.ts +20 -3
  69. package/dist/store/d1-store.d.ts.map +1 -1
  70. package/dist/store/d1-store.js +37 -26
  71. package/dist/store/d1-store.js.map +1 -1
  72. package/package.json +3 -3
@@ -735,116 +735,514 @@ var __connectedAccounts = [];
735
735
  var __oauthProviders = [];
736
736
  var __accountMergeOAuthCleanup = null;
737
737
  var __mergePromptDid = "";
738
+ var __mergePromptCandidates = [];
738
739
  var __providerIcons = {"passkey":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\"><path fill=\"currentColor\" d=\"M3 20v-2.8q0-.85.438-1.562T4.6 14.55q1.55-.775 3.15-1.162T11 13q.5 0 1 .038t1 .112q-.1 1.45.525 2.738T15.35 18v2zm16 3l-1.5-1.5v-4.65q-1.1-.325-1.8-1.237T15 13.5q0-1.45 1.025-2.475T18.5 10t2.475 1.025T22 13.5q0 1.125-.638 2t-1.612 1.25L21 18l-1.5 1.5L21 21zm-8-11q-1.65 0-2.825-1.175T7 8t1.175-2.825T11 4t2.825 1.175T15 8t-1.175 2.825T11 12m7.5 2q.425 0 .713-.288T19.5 13t-.288-.712T18.5 12t-.712.288T17.5 13t.288.713t.712.287\"/></svg>","wallet":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\"><rect x=\"2\" y=\"4\" width=\"20\" height=\"16\" rx=\"3\" fill=\"#4598FA\"/><rect x=\"3\" y=\"6\" width=\"18\" height=\"12\" rx=\"2\" fill=\"white\" opacity=\"0.9\"/><rect x=\"0\" y=\"14\" width=\"24\" height=\"10\" rx=\"0\" fill=\"url(#dw_g)\"/><path d=\"M5.5 9.5h4.5c.3 0 .5.2.5.5v2.5c0 .3-.2.5-.5.5H5.5c-.3 0-.5-.2-.5-.5V10c0-.3.2-.5.5-.5z\" fill=\"#4598FA\"/><defs><linearGradient id=\"dw_g\" x1=\"12\" y1=\"14\" x2=\"12\" y2=\"24\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#77B2F6\"/><stop offset=\"1\" stop-color=\"#4598FA\"/></linearGradient></defs></svg>","did-connect":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\"><rect x=\"2\" y=\"4\" width=\"20\" height=\"16\" rx=\"3\" fill=\"#4598FA\"/><rect x=\"3\" y=\"6\" width=\"18\" height=\"12\" rx=\"2\" fill=\"white\" opacity=\"0.9\"/><rect x=\"0\" y=\"14\" width=\"24\" height=\"10\" rx=\"0\" fill=\"url(#dw_g)\"/><path d=\"M5.5 9.5h4.5c.3 0 .5.2.5.5v2.5c0 .3-.2.5-.5.5H5.5c-.3 0-.5-.2-.5-.5V10c0-.3.2-.5.5-.5z\" fill=\"#4598FA\"/><defs><linearGradient id=\"dw_g\" x1=\"12\" y1=\"14\" x2=\"12\" y2=\"24\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#77B2F6\"/><stop offset=\"1\" stop-color=\"#4598FA\"/></linearGradient></defs></svg>","email":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\"><path fill=\"currentColor\" d=\"M22 7.535V17a3 3 0 0 1-2.824 2.995L19 20H5a3 3 0 0 1-2.995-2.824L2 17V7.535l9.445 6.297l.116.066a1 1 0 0 0 .878 0l.116-.066z\"/><path fill=\"currentColor\" d=\"M19 4c1.08 0 2.027.57 2.555 1.427L12 11.797l-9.555-6.37a2.999 2.999 0 0 1 2.354-1.42L5 4z\"/></svg>","google":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 256 262\"><path fill=\"#4285F4\" d=\"M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027\"/><path fill=\"#34A853\" d=\"M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1\"/><path fill=\"#FBBC05\" d=\"M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z\"/><path fill=\"#EB4335\" d=\"M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251\"/></svg>","github":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 256 250\" fill=\"currentColor\"><path d=\"M128.001 0C57.317 0 0 57.307 0 128.001c0 56.554 36.676 104.535 87.535 121.46c6.397 1.185 8.746-2.777 8.746-6.158c0-3.052-.12-13.135-.174-23.83c-35.61 7.742-43.124-15.103-43.124-15.103c-5.823-14.795-14.213-18.73-14.213-18.73c-11.613-7.944.876-7.78.876-7.78c12.853.902 19.621 13.19 19.621 13.19c11.417 19.568 29.945 13.911 37.249 10.64c1.149-8.272 4.466-13.92 8.127-17.116c-28.431-3.236-58.318-14.212-58.318-63.258c0-13.975 5-25.394 13.188-34.358c-1.329-3.224-5.71-16.242 1.24-33.874c0 0 10.749-3.44 35.21 13.121c10.21-2.836 21.16-4.258 32.038-4.307c10.878.049 21.837 1.47 32.066 4.307c24.431-16.56 35.165-13.12 35.165-13.12c6.967 17.63 2.584 30.65 1.255 33.873c8.207 8.964 13.173 20.383 13.173 34.358c0 49.163-29.944 59.988-58.447 63.157c4.591 3.972 8.682 11.762 8.682 23.704c0 17.126-.148 30.91-.148 35.126c0 3.407 2.304 7.398 8.792 6.14C219.37 232.5 256 184.537 256 128.002C256 57.307 198.691 0 128.001 0\"/></svg>","apple":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 256 315\" fill=\"currentColor\"><path d=\"M213.803 167.03c.442 47.58 41.74 63.413 42.197 63.615c-.35 1.116-6.599 22.563-21.757 44.716c-13.104 19.153-26.705 38.235-48.13 38.63c-21.05.388-27.82-12.483-51.888-12.483c-24.061 0-31.582 12.088-51.51 12.871c-20.68.783-36.428-20.71-49.64-39.793c-27-39.033-47.633-110.3-19.928-158.406c13.763-23.89 38.36-39.017 65.056-39.405c20.307-.388 39.475 13.675 51.889 13.675c12.406 0 35.699-16.895 60.186-14.414c10.25.427 39.026 4.14 57.503 31.186c-1.49.923-34.335 20.044-33.978 59.808M174.24 50.199c10.98-13.29 18.369-31.79 16.353-50.199c-15.826.636-34.962 10.546-46.314 23.828c-10.173 11.763-19.082 30.589-16.678 48.633c17.64 1.365 35.66-8.964 46.639-22.262\"/></svg>","twitter":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 256 256\" fill=\"currentColor\"><path d=\"M149.079 108.399L242.33 0h-22.098l-80.97 94.12L74.59 0H0l97.796 142.328L0 256h22.1l85.507-99.395L175.905 256h74.59L149.073 108.399zM118.81 143.58l-9.909-14.172l-78.84-112.773h33.943l63.625 91.011l9.909 14.173l82.705 118.3H186.3l-67.49-96.533z\"/></svg>","auth0":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 256 256\"><path fill=\"#EB5424\" d=\"M203.97 200.38L167.47 88l-39.5 112.38h75.98zm0-144.76L167.47 168l75.97-26.16l19.76-60.82c6.64-20.4-1.94-42.72-21.18-52.76l-38.06 27.36zM52.03 55.62L88.54 168l39.5-112.38H52.02zM52.03 200.38L88.54 88L12.56 114.16l-5.92 18.2c-6.64 20.4 1.94 42.72 21.18 52.76l24.2-17.38l.01.01v-.01l-.01.01.01-.37z\"/></svg>"};
739
740
  var __hiddenProviders = ["facebook","auth0-legacy"];
740
741
  function toMergeConfirmViewModel(plan) {
741
- const membershipChanges = plan?.membershipChanges ?? [];
742
- const credentials = plan?.credentialsByProvider ?? [];
743
- const skipped = plan?.skippedConflicts ?? [];
744
- const hostCodes = plan?.hostWarningCodes ?? [];
742
+ const raw = plan ?? {};
743
+ function textOrEmpty(value) {
744
+ return typeof value === "string" ? value : "";
745
+ }
746
+ function normalizeProvider(provider) {
747
+ const name = textOrEmpty(provider).trim();
748
+ if (name === "did-connect")
749
+ return "wallet";
750
+ return name;
751
+ }
752
+ function readSide(value) {
753
+ const methods = [];
754
+ const seen = new Set();
755
+ const listed = Array.isArray(value?.methods) ? value.methods : [];
756
+ for (const provider of listed) {
757
+ const name = normalizeProvider(provider);
758
+ if (!name || seen.has(name))
759
+ continue;
760
+ seen.add(name);
761
+ methods.push(name);
762
+ }
763
+ return {
764
+ did: textOrEmpty(value?.did),
765
+ methods,
766
+ createdAt: textOrEmpty(value?.createdAt),
767
+ globalRole: textOrEmpty(value?.globalRole),
768
+ fullName: textOrEmpty(value?.fullName),
769
+ handle: textOrEmpty(value?.handle),
770
+ };
771
+ }
772
+ const movedMethods = [];
773
+ const seenMethods = new Set();
774
+ const credentials = Array.isArray(raw.credentialsByProvider) ? raw.credentialsByProvider : [];
775
+ for (const row of credentials) {
776
+ const name = normalizeProvider(row?.provider);
777
+ if (!name || seenMethods.has(name))
778
+ continue;
779
+ seenMethods.add(name);
780
+ movedMethods.push(name);
781
+ }
782
+ const roleChanges = [];
783
+ const membershipChanges = Array.isArray(raw.membershipChanges) ? raw.membershipChanges : [];
784
+ for (const row of membershipChanges) {
785
+ const intoRoleBefore = textOrEmpty(row?.intoRoleBefore);
786
+ const keptRole = textOrEmpty(row?.keptRole);
787
+ if (!keptRole || keptRole === intoRoleBefore)
788
+ continue;
789
+ roleChanges.push({
790
+ fromRole: textOrEmpty(row?.fromRole),
791
+ intoRoleBefore,
792
+ keptRole,
793
+ instanceName: textOrEmpty(row?.instanceName),
794
+ });
795
+ }
796
+ const movedMemberships = [];
797
+ const moved = Array.isArray(raw.movedMemberships) ? raw.movedMemberships : [];
798
+ for (const row of moved) {
799
+ const instanceName = textOrEmpty(row?.instanceName);
800
+ const role = textOrEmpty(row?.role);
801
+ if (!instanceName && !role)
802
+ continue;
803
+ movedMemberships.push({ instanceName, role });
804
+ }
805
+ const change = raw.globalRoleChange;
806
+ const before = textOrEmpty(change?.before);
807
+ const after = textOrEmpty(change?.after);
808
+ const globalRoleChange = before && after && before !== after ? { before, after } : null;
809
+ const count = raw.accessKeyCount;
810
+ const accessKeyCount = typeof count === "number" && Number.isFinite(count) && count > 0 ? count : 0;
811
+ const recommendedKeep = raw.recommendedKeep === "proved" ? "proved" : "session";
812
+ const reasons = Array.isArray(raw.recommendReasons) ? raw.recommendReasons : [];
745
813
  return {
746
- tooLarge: Boolean(plan?.tooLarge),
747
- survivingDid: plan?.intoDid ?? "",
748
- mergedDid: plan?.fromDid ?? "",
749
- credentialsByProvider: credentials.map((row) => row.provider),
750
- skippedConflicts: skipped,
751
- roleChanges: membershipChanges.filter((row) => row.keptRole !== row.intoRoleBefore),
752
- accessKeyCount: plan?.accessKeyCount ?? 0,
753
- didSpacePointer: plan?.didSpace?.pointer ?? "",
754
- hostWarningCodes: hostCodes,
814
+ tooLarge: raw.tooLarge === true,
815
+ recommendedKeep,
816
+ fromDid: textOrEmpty(raw.fromDid),
817
+ intoDid: textOrEmpty(raw.intoDid),
818
+ session: readSide(raw.accounts?.session),
819
+ proved: readSide(raw.accounts?.proved),
820
+ recommendReasons: reasons.filter((code) => typeof code === "string"),
821
+ movedMethods,
822
+ roleChanges,
823
+ movedMemberships,
824
+ globalRoleChange,
825
+ accessKeyCount,
826
+ mergedHandle: textOrEmpty(raw.mergedHandle),
827
+ matchesCandidate: raw.matchesCandidate === true,
755
828
  };
756
829
  }
757
- function renderMergeConfirmBodyHtml(plan, t, escapeHtml, providerLabel) {
830
+ function renderMergeConfirmBodyHtml(plan, t, escapeHtml, providerLabel, options) {
758
831
  const view = toMergeConfirmViewModel(plan);
759
- const roleLabel = (role) => {
832
+ const requested = options?.selectedKeep;
833
+ const picked = requested === "session" || requested === "proved" ? requested : view.recommendedKeep;
834
+ function plain(value) {
835
+ return String(value ?? "").replace(/[{}]/g, "");
836
+ }
837
+ function text(key, params) {
838
+ let safe;
839
+ if (params) {
840
+ safe = {};
841
+ for (const name of Object.keys(params))
842
+ safe[name] = plain(params[name]);
843
+ }
844
+ return escapeHtml(t(key, safe));
845
+ }
846
+ function shortOf(did) {
847
+ if (typeof options?.shortDid === "function")
848
+ return options.shortDid(did);
849
+ if (!did)
850
+ return "";
851
+ if (did.length <= 17)
852
+ return did;
853
+ return `${did.slice(0, 8)}...${did.slice(-6)}`;
854
+ }
855
+ function roleLabel(role) {
760
856
  const key = `common.${role}`;
761
857
  const translated = t(key);
762
858
  return translated === key ? role : translated;
763
- };
764
- const labelFor = (provider) => (providerLabel ? providerLabel(provider) : provider);
765
- const skippedReason = (reason) => reason === "UNIQUE(provider,id)" ? t("profile.mergeConfirmSkippedReason") : reason;
766
- const hostWarning = (code) => {
767
- if (code === "blocklet-owner-clash")
768
- return t("profile.mergeHostBlockletOwnerClash");
769
- if (code === "external-token-rebind")
770
- return t("profile.mergeHostExternalTokenRebind");
771
- return code;
772
- };
773
- const wrapDid = (did) => `<code class="merge-confirm-did">${escapeHtml(did)}</code>`;
774
- const credentialItems = view.credentialsByProvider
775
- .map((provider) => `<li>${escapeHtml(labelFor(provider))}</li>`)
776
- .join("");
777
- const skippedItems = view.skippedConflicts
778
- .map((row) => `<li>${escapeHtml(labelFor(row.provider))} — ${escapeHtml(skippedReason(row.reason))}</li>`)
779
- .join("");
780
- const roleItems = view.roleChanges
781
- .map((row) => `<li>${escapeHtml(t("profile.mergeConfirmRoleChange", {
782
- fromRole: roleLabel(row.fromRole),
783
- intoRoleBefore: roleLabel(row.intoRoleBefore),
784
- keptRole: roleLabel(row.keptRole),
785
- }))}</li>`)
786
- .join("");
787
- const hostItems = view.hostWarningCodes
788
- .map((code) => `<li>${escapeHtml(hostWarning(code))}</li>`)
789
- .join("");
790
- let html = `<p class="dialog-desc">${t("profile.mergeConfirmSurviving", {
791
- intoDid: wrapDid(view.survivingDid),
792
- fromDid: wrapDid(view.mergedDid),
793
- })}</p>`;
794
- html += `<p class="merge-confirm-keep-other">${t("profile.mergeConfirmKeepOther")}</p>`;
795
- if (credentialItems) {
796
- html += `<div class="merge-confirm-section"><h4>${escapeHtml(t("profile.mergeConfirmCredentials"))}</h4><ul class="merge-confirm-list">${credentialItems}</ul></div>`;
797
- }
798
- if (skippedItems) {
799
- html += `<div class="merge-confirm-section"><h4>${escapeHtml(t("profile.mergeConfirmSkipped"))}</h4><ul class="merge-confirm-list">${skippedItems}</ul></div>`;
800
- }
801
- if (roleItems) {
802
- html += `<div class="merge-confirm-section"><h4>${escapeHtml(t("profile.mergeConfirmRoles"))}</h4><ul class="merge-confirm-list">${roleItems}</ul></div>`;
803
- }
804
- if (view.accessKeyCount > 0) {
805
- html += `<div class="merge-confirm-section"><h4>${escapeHtml(t("profile.mergeConfirmAccessKeysTitle"))}</h4><p class="merge-confirm-note">${escapeHtml(t("profile.mergeConfirmAccessKeys", { count: String(view.accessKeyCount) }))}</p></div>`;
806
- }
807
- html += `<div class="merge-confirm-section"><h4>${escapeHtml(t("profile.mergeConfirmDidSpaceTitle"))}</h4><p class="merge-confirm-note">${t("profile.mergeDidSpace", {
808
- pointer: wrapDid(view.didSpacePointer),
809
- })}</p></div>`;
810
- if (hostItems) {
811
- html += `<div class="merge-confirm-section"><h4>${escapeHtml(t("profile.mergeConfirmHostWarnings"))}</h4><ul class="merge-confirm-list">${hostItems}</ul></div>`;
812
- }
813
- html += `<p class="merge-confirm-irreversible">${escapeHtml(t("profile.mergeConfirmIrreversible"))}</p>`;
814
- if (view.tooLarge) {
815
- html += `<p class="merge-confirm-too-large">${escapeHtml(t("profile.mergeConfirmTooLarge"))}</p>`;
816
859
  }
817
- return html;
818
- }
819
- function renderMergeMethodPickerHtml(options, t, escapeHtml, iconFor, labelFor) {
820
- const buttons = (options || [])
821
- .map((opt) => {
860
+ function methodLabels(methods) {
861
+ const labels = [];
862
+ for (const provider of methods) {
863
+ labels.push(typeof providerLabel === "function" ? providerLabel(provider) : provider);
864
+ }
865
+ return labels.join(t("profile.mergePromptMethodSep"));
866
+ }
867
+ function createdOn(iso) {
868
+ const match = /^(\d{4}-\d{2}-\d{2})/.exec(iso);
869
+ return match?.[1] ?? iso;
870
+ }
871
+ function accountCard(keep) {
872
+ const account = keep === "session" ? view.session : view.proved;
873
+ const lines = [];
874
+ const nameBits = [];
875
+ if (account.fullName)
876
+ nameBits.push(account.fullName);
877
+ if (account.handle)
878
+ nameBits.push(`@${account.handle}`);
879
+ if (nameBits.length > 0) {
880
+ lines.push(`<span class="merge-confirm-line">${escapeHtml(nameBits.join(" · "))}</span>`);
881
+ }
882
+ if (account.methods.length > 0) {
883
+ lines.push(`<span class="merge-confirm-line">${escapeHtml(methodLabels(account.methods))}</span>`);
884
+ }
885
+ const meta = [];
886
+ if (account.globalRole)
887
+ meta.push(roleLabel(account.globalRole));
888
+ if (account.createdAt)
889
+ meta.push(t("profile.mergeConfirmCreated", { date: plain(createdOn(account.createdAt)) }));
890
+ if (meta.length > 0)
891
+ lines.push(`<span class="merge-confirm-line">${escapeHtml(meta.join(" · "))}</span>`);
892
+ if (keep === "session") {
893
+ lines.push(`<span class="merge-confirm-current">${text("profile.mergeConfirmCurrent")}</span>`);
894
+ }
895
+ const copy = account.did
896
+ ? `<button type="button" class="merge-confirm-copy" data-copy-did="${escapeHtml(account.did)}">${text("common.copy")}</button>`
897
+ : "";
898
+ const badge = view.recommendedKeep === keep
899
+ ? `<span class="merge-confirm-badge">${text("profile.mergeConfirmRecommended")}</span>`
900
+ : "";
901
+ const selected = picked === keep ? " checked" : "";
902
+ const selectedClass = picked === keep ? " is-selected" : "";
903
+ return (`<label class="merge-confirm-card${selectedClass}" data-keep="${keep}">` +
904
+ `<input type="radio" name="merge-keep" value="${keep}"${selected}>` +
905
+ `<span class="merge-confirm-card-main">` +
906
+ `<span class="merge-confirm-card-head"><span class="merge-confirm-did">${escapeHtml(shortOf(account.did))}</span>${copy}${badge}</span>` +
907
+ lines.join("") +
908
+ `</span></label>`);
909
+ }
910
+ let banner = "";
911
+ if (options?.hasCandidate === true && view.matchesCandidate && options.candidateEmail) {
912
+ banner = `<p class="merge-confirm-match">${text("profile.mergeConfirmMatch", { email: options.candidateEmail })}</p>`;
913
+ }
914
+ else if (options?.hasCandidate === true && !view.matchesCandidate) {
915
+ banner = `<p class="merge-confirm-not-match">${text("profile.mergeConfirmNotMatch")}</p>`;
916
+ }
917
+ const primary = view.recommendedKeep === "proved" ? "proved" : "session";
918
+ const secondary = primary === "session" ? "proved" : "session";
919
+ const recommendedAccount = view.recommendedKeep === "proved" ? view.proved : view.session;
920
+ const reasonBits = [];
921
+ for (const code of view.recommendReasons) {
922
+ if (code === "global-role" || code === "instance-role")
923
+ reasonBits.push(t("profile.mergeConfirmReasonHigher"));
924
+ else if (code === "handle") {
925
+ reasonBits.push(t("profile.mergeConfirmReasonHandle", { handle: plain(recommendedAccount.handle) }));
926
+ }
927
+ else if (code === "wallet-did")
928
+ reasonBits.push(t("profile.mergeConfirmReasonWallet"));
929
+ else if (code === "older")
930
+ reasonBits.push(t("profile.mergeConfirmReasonOlder"));
931
+ }
932
+ const reason = reasonBits.length
933
+ ? text("profile.mergeConfirmReasonLead", {
934
+ did: shortOf(recommendedAccount.did),
935
+ reason: reasonBits.join(" "),
936
+ })
937
+ : text("profile.mergeConfirmReasonSimilar");
938
+ let changes = "";
939
+ if (options?.loading === true) {
940
+ changes =
941
+ `<div id="merge-confirm-changes" class="merge-confirm-section">` +
942
+ `<h4>${text("profile.mergeConfirmChanges")}</h4>` +
943
+ `<p class="merge-confirm-loading">${text("profile.mergeConfirmLoading")}</p>` +
944
+ `</div>`;
945
+ }
946
+ else {
947
+ const rows = [];
948
+ if (view.movedMethods.length > 0) {
949
+ rows.push(`<li>${text("profile.mergeConfirmMovedMethods", { methods: methodLabels(view.movedMethods) })}</li>`);
950
+ }
951
+ if (view.globalRoleChange) {
952
+ rows.push(`<li>${text("profile.mergeConfirmGlobalRole", {
953
+ before: roleLabel(view.globalRoleChange.before),
954
+ after: roleLabel(view.globalRoleChange.after),
955
+ })}</li>`);
956
+ }
957
+ for (const row of view.roleChanges) {
958
+ const before = roleLabel(row.intoRoleBefore);
959
+ const after = roleLabel(row.keptRole);
960
+ const line = row.instanceName
961
+ ? text("profile.mergeConfirmSpaceRole", { space: row.instanceName, before, after })
962
+ : text("profile.mergeConfirmSpaceRoleUnnamed", { before, after });
963
+ rows.push(`<li>${line}</li>`);
964
+ }
965
+ if (view.movedMemberships.length > 0) {
966
+ const parts = [];
967
+ for (const row of view.movedMemberships) {
968
+ const role = roleLabel(row.role);
969
+ parts.push(row.instanceName
970
+ ? t("profile.mergeConfirmJoinedSpace", { space: plain(row.instanceName), role: plain(role) })
971
+ : t("profile.mergeConfirmJoinedSpaceUnnamed", { role: plain(role) }));
972
+ }
973
+ rows.push(`<li>${text("profile.mergeConfirmJoinedSpaces", {
974
+ spaces: parts.join(t("profile.mergePromptMethodSep")),
975
+ })}</li>`);
976
+ }
977
+ if (view.accessKeyCount > 0) {
978
+ rows.push(`<li>${text("profile.mergeConfirmAccessKeys", { count: String(view.accessKeyCount) })}</li>`);
979
+ }
980
+ if (view.mergedHandle) {
981
+ rows.push(`<li>${text("profile.mergeConfirmHandleStays", {
982
+ handle: view.mergedHandle,
983
+ did: shortOf(view.fromDid),
984
+ })}</li>`);
985
+ }
986
+ rows.push(`<li>${text("profile.mergeConfirmNameAvatar", { did: shortOf(view.fromDid) })}</li>`);
987
+ rows.push(`<li>${text("profile.mergeConfirmDidSpaceNotCopied", { did: shortOf(view.fromDid) })}</li>`);
988
+ const tooLarge = view.tooLarge
989
+ ? `<p class="merge-confirm-too-large">${text("profile.mergeConfirmTooLarge")}</p>`
990
+ : "";
991
+ changes =
992
+ `<div id="merge-confirm-changes" class="merge-confirm-section">` +
993
+ `<h4>${text("profile.mergeConfirmChanges")}</h4>` +
994
+ `<ul class="merge-confirm-list">${rows.join("")}</ul>` +
995
+ `<p class="merge-confirm-host">${text("profile.mergeConfirmHostMaybe", { did: shortOf(view.fromDid) })}</p>` +
996
+ `<p class="merge-confirm-irreversible">${text("profile.mergeConfirmIrreversible")}</p>` +
997
+ tooLarge +
998
+ `</div>`;
999
+ }
1000
+ return (banner +
1001
+ `<p class="merge-confirm-choose">${text("profile.mergeConfirmChoose")}</p>` +
1002
+ `<div class="merge-confirm-cards">${accountCard(primary)}${accountCard(secondary)}</div>` +
1003
+ `<p class="merge-confirm-reason">${reason}</p>` +
1004
+ `<p class="merge-confirm-alias">${text("profile.mergeConfirmAlias")}</p>` +
1005
+ changes);
1006
+ }
1007
+ function renderMergeDoneBodyHtml(input, t, escapeHtml, providerLabel, shortDid) {
1008
+ const data = input ?? {};
1009
+ const did = typeof data.survivorDid === "string" ? data.survivorDid : "";
1010
+ function plain(value) {
1011
+ return String(value ?? "").replace(/[{}]/g, "");
1012
+ }
1013
+ function text(key, params) {
1014
+ let safe;
1015
+ if (params) {
1016
+ safe = {};
1017
+ for (const name of Object.keys(params))
1018
+ safe[name] = plain(params[name]);
1019
+ }
1020
+ return escapeHtml(t(key, safe));
1021
+ }
1022
+ function shortOf(value) {
1023
+ if (typeof shortDid === "function")
1024
+ return shortDid(value);
1025
+ if (!value)
1026
+ return "";
1027
+ if (value.length <= 17)
1028
+ return value;
1029
+ return `${value.slice(0, 8)}...${value.slice(-6)}`;
1030
+ }
1031
+ const seen = new Set();
1032
+ const labels = [];
1033
+ const methods = Array.isArray(data.survivorMethods) ? data.survivorMethods : [];
1034
+ for (const provider of methods) {
1035
+ if (typeof provider !== "string")
1036
+ continue;
1037
+ const name = provider.trim() === "did-connect" ? "wallet" : provider.trim();
1038
+ if (!name || seen.has(name))
1039
+ continue;
1040
+ seen.add(name);
1041
+ labels.push(typeof providerLabel === "function" ? providerLabel(name) : name);
1042
+ }
1043
+ const didLine = `<p class="merge-confirm-done-did">${text("profile.mergeDoneDid", { did: shortOf(did) })}</p>`;
1044
+ if (labels.length === 0)
1045
+ return didLine;
1046
+ return (didLine +
1047
+ `<p class="merge-confirm-done-methods">${text("profile.mergeDoneMethods", {
1048
+ methods: labels.join(t("profile.mergePromptMethodSep")),
1049
+ })}</p>`);
1050
+ }
1051
+ function renderMergeMethodPickerHtml(options, t, escapeHtml, iconFor, labelFor, errorText, guide) {
1052
+ const button = (opt, primary) => {
1053
+ const cls = primary ? "btn merge-method-btn" : "btn btn-secondary merge-method-btn";
822
1054
  if (opt.method === "wallet") {
823
- return (`<button type="button" class="btn btn-secondary merge-method-btn" data-merge-method="wallet">` +
1055
+ return (`<button type="button" class="${cls}" data-merge-method="wallet">` +
824
1056
  `${iconFor("did-connect")} ${escapeHtml(t("profile.mergeProveWallet"))}</button>`);
825
1057
  }
826
1058
  if (opt.method === "passkey") {
827
- return (`<button type="button" class="btn btn-secondary merge-method-btn" data-merge-method="passkey">` +
1059
+ return (`<button type="button" class="${cls}" data-merge-method="passkey">` +
828
1060
  `${iconFor("passkey")} ${escapeHtml(t("profile.mergeProvePasskey"))}</button>`);
829
1061
  }
830
1062
  if (opt.method === "email") {
831
- return (`<button type="button" class="btn btn-secondary merge-method-btn" data-merge-method="email">` +
832
- `${iconFor("email")} ${escapeHtml(t("profile.mergeProveEmail"))}</button>`);
1063
+ const label = opt.email ? t("profile.mergeProveEmailAddress", { email: opt.email }) : t("profile.mergeProveEmail");
1064
+ const emailAttr = opt.email ? ` data-merge-email="${escapeHtml(opt.email)}"` : "";
1065
+ return (`<button type="button" class="${cls}" data-merge-method="email"${emailAttr}>` +
1066
+ `${iconFor("email")} ${escapeHtml(label)}</button>`);
833
1067
  }
834
1068
  const provider = opt.provider || "";
835
- return (`<button type="button" class="btn btn-secondary merge-method-btn" data-merge-method="oauth" data-merge-provider="${escapeHtml(provider)}">` +
1069
+ return (`<button type="button" class="${cls}" data-merge-method="oauth" data-merge-provider="${escapeHtml(provider)}">` +
836
1070
  `${iconFor(provider)} ${escapeHtml(t("profile.mergeProveOAuth", { provider: labelFor(provider) }))}</button>`);
837
- })
838
- .join("");
1071
+ };
1072
+ const alert = errorText
1073
+ ? `<p id="account-merge-method-error" class="dialog-desc" role="alert">${escapeHtml(errorText)}</p>`
1074
+ : "";
1075
+ const hintKey = guide ? "profile.mergeCandidateProveHint" : "profile.mergeAccountHint";
1076
+ let listHtml = "";
1077
+ if (guide) {
1078
+ if (guide.unavailable) {
1079
+ listHtml += `<p class="merge-method-unavailable">${escapeHtml(t("profile.mergePromptMethodsDisabled"))}</p>`;
1080
+ }
1081
+ else {
1082
+ const recommended = (guide.recommended || []).map((opt) => button(opt, true)).join("");
1083
+ listHtml +=
1084
+ `<div class="merge-method-recommended">` +
1085
+ `<p class="merge-method-kicker">${escapeHtml(t("profile.mergePromptMethodsLabel"))}</p>` +
1086
+ `<div class="merge-method-list">${recommended}</div></div>`;
1087
+ }
1088
+ const others = guide.others || [];
1089
+ if (others.length) {
1090
+ const open = guide.othersOpen ? " open" : "";
1091
+ listHtml +=
1092
+ `<details class="merge-method-others"${open}>` +
1093
+ `<summary>${escapeHtml(t("profile.mergePromptOtherMethods"))}</summary>` +
1094
+ `<div class="merge-method-list">${others.map((opt) => button(opt, false)).join("")}</div></details>`;
1095
+ }
1096
+ }
1097
+ else {
1098
+ listHtml = `<div class="merge-method-list">${(options || []).map((opt) => button(opt, false)).join("")}</div>`;
1099
+ }
839
1100
  return (`<div class="dialog-content">` +
840
1101
  `<h3>${escapeHtml(t("profile.mergeChooseMethod"))}</h3>` +
841
- `<p class="dialog-desc">${escapeHtml(t("profile.mergeAccountHint"))}</p>` +
842
- `<div class="merge-method-list">${buttons}</div>` +
1102
+ `<p class="dialog-desc">${escapeHtml(t(hintKey))}</p>` +
1103
+ alert +
1104
+ listHtml +
843
1105
  `</div>` +
844
1106
  `<div class="dialog-actions">` +
845
1107
  `<button type="button" class="btn btn-secondary" id="account-merge-method-cancel">${escapeHtml(t("common.cancel"))}</button>` +
846
1108
  `</div>`);
847
1109
  }
1110
+ function formatMergeCandidateWhen(iso, nowMs, lang, t) {
1111
+ if (!iso)
1112
+ return "";
1113
+ const then = new Date(iso).getTime();
1114
+ if (Number.isNaN(then))
1115
+ return "";
1116
+ const dayMs = 86400000;
1117
+ let days = Math.floor((nowMs - then) / dayMs);
1118
+ if (days < 0)
1119
+ days = 0;
1120
+ if (days >= 365) {
1121
+ const locale = lang || "en";
1122
+ try {
1123
+ return new Intl.DateTimeFormat(locale, { year: "numeric", month: "long" }).format(new Date(then));
1124
+ }
1125
+ catch {
1126
+ return new Intl.DateTimeFormat("en", { year: "numeric", month: "long" }).format(new Date(then));
1127
+ }
1128
+ }
1129
+ if (days >= 30) {
1130
+ const months = Math.max(1, Math.floor(days / 30));
1131
+ return months === 1 ? t("profile.mergePromptMonthAgo") : t("profile.mergePromptMonthsAgo", { count: String(months) });
1132
+ }
1133
+ if (days === 0)
1134
+ return t("profile.mergePromptToday");
1135
+ if (days === 1)
1136
+ return t("profile.mergePromptDayAgo");
1137
+ return t("profile.mergePromptDaysAgo", { count: String(days) });
1138
+ }
1139
+ function partitionMergeMethods(candidate, enabled, hiddenProviders) {
1140
+ const enabledList = enabled || [];
1141
+ const hidden = hiddenProviders || [];
1142
+ const methods = (candidate && candidate.methods) || [];
1143
+ const emailLogin = !!(candidate && candidate.emailLogin === true);
1144
+ const email = (candidate && typeof candidate.email === "string" && candidate.email) || "";
1145
+ const recommended = [];
1146
+ const used = {};
1147
+ const isHidden = (name) => hidden.indexOf(name) !== -1;
1148
+ const findEnabled = (method, provider) => {
1149
+ for (let i = 0; i < enabledList.length; i++) {
1150
+ const opt = enabledList[i];
1151
+ if (!opt || opt.method !== method)
1152
+ continue;
1153
+ if (method === "oauth" && opt.provider !== provider)
1154
+ continue;
1155
+ return opt;
1156
+ }
1157
+ return null;
1158
+ };
1159
+ for (let m = 0; m < methods.length; m++) {
1160
+ const name = methods[m];
1161
+ if (!name || isHidden(name) || name === "email")
1162
+ continue;
1163
+ const method = name === "wallet" || name === "passkey" ? name : "oauth";
1164
+ const provider = method === "oauth" ? name : "";
1165
+ const hit = findEnabled(method, provider);
1166
+ if (!hit)
1167
+ continue;
1168
+ const key = `${method}:${hit.provider || ""}`;
1169
+ if (used[key])
1170
+ continue;
1171
+ used[key] = true;
1172
+ if (method === "oauth")
1173
+ recommended.push({ method: "oauth", provider: hit.provider || provider });
1174
+ else if (method === "wallet")
1175
+ recommended.push({ method: "wallet" });
1176
+ else
1177
+ recommended.push({ method: "passkey" });
1178
+ }
1179
+ if (emailLogin && email && findEnabled("email", "")) {
1180
+ recommended.push({ method: "email", email });
1181
+ used["email:"] = true;
1182
+ }
1183
+ const others = [];
1184
+ for (let e = 0; e < enabledList.length; e++) {
1185
+ const opt = enabledList[e];
1186
+ if (!opt || opt.method === "email")
1187
+ continue;
1188
+ const key = `${opt.method}:${opt.provider || ""}`;
1189
+ if (used[key])
1190
+ continue;
1191
+ others.push(opt.method === "oauth" ? { method: "oauth", provider: opt.provider || "" } : { method: opt.method });
1192
+ }
1193
+ const unavailable = recommended.length === 0;
1194
+ return { recommended, others, othersOpen: unavailable, unavailable };
1195
+ }
1196
+ function renderMergePromptHtml(email, candidates, t, escapeHtml, iconFor, labelFor, formatWhen, hiddenProviders) {
1197
+ const hidden = hiddenProviders || [];
1198
+ const rows = candidates || [];
1199
+ const many = rows.length > 1;
1200
+ const title = many
1201
+ ? t("profile.mergePromptTitleMany", { email, count: String(rows.length) })
1202
+ : t("profile.mergePromptTitleOne", { email });
1203
+ const body = many ? t("profile.mergePromptBodyMany") : t("profile.mergePromptBodyOne");
1204
+ let html = `<div class="ca-merge-prompt-title">${escapeHtml(title)}</div>` +
1205
+ `<p class="ca-merge-prompt-body">${escapeHtml(body)}</p>`;
1206
+ for (let i = 0; i < rows.length; i++) {
1207
+ const row = rows[i];
1208
+ if (!row)
1209
+ continue;
1210
+ const methods = row.methods || [];
1211
+ const icons = [];
1212
+ const names = [];
1213
+ for (let m = 0; m < methods.length; m++) {
1214
+ const name = methods[m];
1215
+ if (!name || hidden.indexOf(name) !== -1)
1216
+ continue;
1217
+ icons.push(`<span class="ca-merge-prompt-icon">${iconFor(name)}</span>`);
1218
+ names.push(escapeHtml(labelFor(name)));
1219
+ }
1220
+ const bits = [];
1221
+ if (names.length) {
1222
+ bits.push(`<span class="ca-merge-prompt-methods">${icons.join("")}` +
1223
+ `<span class="ca-merge-prompt-method-names">${names.join(escapeHtml(t("profile.mergePromptMethodSep")))}</span></span>`);
1224
+ }
1225
+ const created = row.createdAt ? formatWhen(row.createdAt) : "";
1226
+ if (created)
1227
+ bits.push(escapeHtml(t("profile.mergePromptCreated", { when: created })));
1228
+ const last = row.lastLoginAt ? formatWhen(row.lastLoginAt) : "";
1229
+ if (last)
1230
+ bits.push(escapeHtml(t("profile.mergePromptLastLogin", { when: last })));
1231
+ const ref = escapeHtml(row.ref || "");
1232
+ html +=
1233
+ `<div class="ca-merge-prompt-row">` +
1234
+ `<div class="ca-merge-prompt-meta">${bits.join('<span class="ca-merge-prompt-dot"> · </span>')}</div>` +
1235
+ `<div class="ca-merge-prompt-actions">` +
1236
+ `<button type="button" class="btn" data-merge-ref="${ref}" onclick="startMergeCandidate(this)">${escapeHtml(t("profile.mergePromptMergeThis"))}</button>` +
1237
+ `<button type="button" class="btn btn-secondary" data-merge-ref="${ref}" onclick="hideMergeCandidate(this)">${escapeHtml(t("profile.mergePromptNotMine"))}</button>` +
1238
+ `</div></div>`;
1239
+ }
1240
+ html +=
1241
+ `<div class="ca-merge-prompt-later">` +
1242
+ `<button type="button" class="btn btn-secondary" id="merge-candidate-prompt-later" onclick="dismissMergePromptLater()">` +
1243
+ `${escapeHtml(t("profile.mergePromptLater"))}</button></div>`;
1244
+ return html;
1245
+ }
848
1246
 
849
1247
  async function loadConnectedAccounts() {
850
1248
  var card = document.getElementById("connected-accounts-card");
@@ -979,7 +1377,7 @@ function renderConnectedAccounts() {
979
1377
  html += '<div class="ca-section">';
980
1378
  html += '<div class="ca-section-label">' + __t("profile.mergeAccount") + '</div>';
981
1379
  html += '<p class="ca-merge-hint" style="margin:0 0 8px;color:var(--text-secondary);font-size:13px;">' + __t("profile.mergeAccountHint") + '</p>';
982
- html += '<button class="btn btn-secondary" onclick="startAccountMerge()">' + __t("profile.mergeChooseMethod") + '</button>';
1380
+ html += '<button class="btn btn-secondary" onclick="startAccountMerge()">' + __t("profile.mergeStart") + '</button>';
983
1381
  html += '</div>';
984
1382
 
985
1383
  list.innerHTML = html;
@@ -1310,17 +1708,29 @@ function cleanupAccountMergeOAuth() {
1310
1708
  if (__accountMergeOAuthCleanup) __accountMergeOAuthCleanup();
1311
1709
  }
1312
1710
 
1711
+ function mergeChallengeLooksExpired(error) {
1712
+ return error === "Invalid or expired merge challenge"
1713
+ || error === "合并验证无效或已过期"
1714
+ || error === "合併驗證無效或已過期"
1715
+ || error === "統合チャレンジが無効か、期限切れです";
1716
+ }
1717
+
1718
+ function emailCodeAlreadySent(error) {
1719
+ return error === "Code already sent, please wait"
1720
+ || error === "验证码已发送,请稍候"
1721
+ || error === "驗證碼已寄出,請稍候"
1722
+ || error === "確認コードは送信済みです。しばらくお待ちください";
1723
+ }
1724
+
1313
1725
  async function proveAccountMergeWithWallet(mergeChallenge) {
1314
1726
  var tokenData;
1315
1727
  try {
1316
- tokenData = await api("GET", "/../did/merge-proof/token");
1728
+ tokenData = await api("GET", "/../did/merge-proof/token", undefined, { silent: true });
1317
1729
  } catch (e) {
1318
- showToast(__t("profile.mergeFailed"), "error");
1319
- return null;
1730
+ return { ok: false, error: __t("profile.mergeFailed") };
1320
1731
  }
1321
1732
  if (!tokenData || !tokenData.token) {
1322
- showToast(__t("profile.mergeFailed"), "error");
1323
- return null;
1733
+ return { ok: false, error: (tokenData && tokenData.error) || __t("profile.mergeFailed") };
1324
1734
  }
1325
1735
 
1326
1736
  var token = tokenData.token;
@@ -1396,17 +1806,16 @@ async function proveAccountMergeWithWallet(mergeChallenge) {
1396
1806
  var proved = await api("POST", "/../did/connect/complete", {
1397
1807
  token: token,
1398
1808
  mergeChallenge: mergeChallenge,
1399
- });
1809
+ }, { silent: true });
1400
1810
  if (!proved || !proved.mergeIntent) {
1401
- statusEl.textContent = (proved && proved.error) || __t("profile.mergeFailed");
1402
- polling = false;
1811
+ finish(proved || { ok: false, error: __t("profile.mergeFailed") });
1403
1812
  return;
1404
1813
  }
1405
1814
  finish(proved);
1406
1815
  } else if (status.status === "error") {
1407
1816
  clearInterval(pollTimer);
1408
1817
  pollTimer = null;
1409
- statusEl.textContent = status.error || __t("profile.mergeFailed");
1818
+ finish({ ok: false, error: status.error || __t("profile.mergeFailed") });
1410
1819
  }
1411
1820
  } catch (e) { /* ignore poll errors */ }
1412
1821
  polling = false;
@@ -1416,14 +1825,12 @@ async function proveAccountMergeWithWallet(mergeChallenge) {
1416
1825
 
1417
1826
  async function proveAccountMergeWithPasskey(mergeChallenge) {
1418
1827
  if (typeof PublicKeyCredential === "undefined" || !navigator.credentials || !navigator.credentials.get) {
1419
- showToast(__t("profile.mergePasskeyUnavailable"), "error");
1420
- return null;
1828
+ return { ok: false, error: __t("profile.mergePasskeyUnavailable") };
1421
1829
  }
1422
- var opts = await api("GET", "/../passkey/auth");
1830
+ var opts = await api("GET", "/../passkey/auth", undefined, { silent: true });
1423
1831
  var publicKey = opts.authentication || opts;
1424
- if (!opts.challengeId || !publicKey || !publicKey.challenge) {
1425
- showToast((opts && opts.error) || __t("profile.mergeFailed"), "error");
1426
- return null;
1832
+ if (!opts || opts.ok === false || !opts.challengeId || !publicKey || !publicKey.challenge) {
1833
+ return { ok: false, error: (opts && opts.error) || __t("profile.mergeFailed") };
1427
1834
  }
1428
1835
  publicKey.challenge = base64urlToBuffer(publicKey.challenge);
1429
1836
  if (publicKey.allowCredentials) {
@@ -1436,7 +1843,7 @@ async function proveAccountMergeWithPasskey(mergeChallenge) {
1436
1843
  credential = await navigator.credentials.get({ publicKey: publicKey });
1437
1844
  } catch (e) {
1438
1845
  if (!e || e.name !== "NotAllowedError") {
1439
- showToast((e && e.message) || __t("profile.mergeFailed"), "error");
1846
+ return { ok: false, error: (e && e.message) || __t("profile.mergeFailed") };
1440
1847
  }
1441
1848
  return null;
1442
1849
  }
@@ -1456,7 +1863,7 @@ async function proveAccountMergeWithPasskey(mergeChallenge) {
1456
1863
  },
1457
1864
  },
1458
1865
  mergeChallenge: mergeChallenge,
1459
- });
1866
+ }, { silent: true });
1460
1867
  }
1461
1868
 
1462
1869
  function proveAccountMergeWithOAuth(provider, mergeChallenge) {
@@ -1466,15 +1873,23 @@ function proveAccountMergeWithOAuth(provider, mergeChallenge) {
1466
1873
  return;
1467
1874
  }
1468
1875
  cleanupAccountMergeOAuth();
1876
+ var mergeResultPrefix = "connect.mergeOAuthResult.";
1877
+ try {
1878
+ var staleKeys = [];
1879
+ for (var s = 0; s < localStorage.length; s++) {
1880
+ var staleKey = localStorage.key(s);
1881
+ if (staleKey && staleKey.indexOf(mergeResultPrefix) === 0) staleKeys.push(staleKey);
1882
+ }
1883
+ for (var t = 0; t < staleKeys.length; t++) localStorage.removeItem(staleKeys[t]);
1884
+ } catch (e0) {}
1469
1885
  var returnUrl = encodeURIComponent(window.location.href);
1470
- var url = window.__BASE_PATH + "/../oauth/" + encodeURIComponent(provider) + "/login?returnUrl=" + returnUrl;
1886
+ var url = window.__BASE_PATH + "/../oauth/" + encodeURIComponent(provider) + "/login?returnUrl=" + returnUrl + "&mergeChallenge=" + encodeURIComponent(mergeChallenge);
1471
1887
  var redirectUri = location.origin + "/.well-known/service/oauth/callback/" + encodeURIComponent(provider);
1472
1888
  var w = 500, h = 600;
1473
1889
  var left = (screen.width - w) / 2, top = (screen.height - h) / 2;
1474
1890
  var popup = window.open(url, "oauth_merge", "width=" + w + ",height=" + h + ",left=" + left + ",top=" + top);
1475
1891
  if (!popup) {
1476
- showToast(__t("common.popupBlocked"), "error");
1477
- resolve(null);
1892
+ resolve({ ok: false, error: __t("common.popupBlocked") });
1478
1893
  return;
1479
1894
  }
1480
1895
  var settled = false;
@@ -1485,6 +1900,7 @@ function proveAccountMergeWithOAuth(provider, mergeChallenge) {
1485
1900
  }, 5 * 60 * 1000);
1486
1901
  function cleanup() {
1487
1902
  window.removeEventListener("message", onMessage);
1903
+ window.removeEventListener("storage", onStorage);
1488
1904
  clearTimeout(abandonTimer);
1489
1905
  if (__accountMergeOAuthCleanup === cancel) __accountMergeOAuthCleanup = null;
1490
1906
  }
@@ -1494,6 +1910,37 @@ function proveAccountMergeWithOAuth(provider, mergeChallenge) {
1494
1910
  cleanup();
1495
1911
  resolve(value);
1496
1912
  }
1913
+ function parseMergeOAuthResult(raw) {
1914
+ if (!raw) return null;
1915
+ try {
1916
+ var parsed = JSON.parse(raw);
1917
+ if (!parsed || typeof parsed !== "object") return { ok: false, error: __t("profile.mergeFailed") };
1918
+ return parsed;
1919
+ } catch (e1) {
1920
+ return { ok: false, error: __t("profile.mergeFailed") };
1921
+ }
1922
+ }
1923
+ function readMergeOAuthResultOnce() {
1924
+ try {
1925
+ for (var i = 0; i < localStorage.length; i++) {
1926
+ var key = localStorage.key(i);
1927
+ if (!key || key.indexOf(mergeResultPrefix) !== 0) continue;
1928
+ var raw = localStorage.getItem(key);
1929
+ try { localStorage.removeItem(key); } catch (e2) {}
1930
+ return parseMergeOAuthResult(raw);
1931
+ }
1932
+ } catch (e3) {}
1933
+ return null;
1934
+ }
1935
+ function onStorage(event) {
1936
+ if (settled || !event || !event.key || event.key.indexOf(mergeResultPrefix) !== 0) return;
1937
+ var raw = event.newValue;
1938
+ try { localStorage.removeItem(event.key); } catch (e4) {}
1939
+ var payload = parseMergeOAuthResult(raw);
1940
+ if (!payload) return;
1941
+ try { popup.close(); } catch (e5) {}
1942
+ finish(payload);
1943
+ }
1497
1944
  async function onMessage(event) {
1498
1945
  if (event.origin !== location.origin) return;
1499
1946
  if (!event.data || event.data.type !== "authorization_response") return;
@@ -1511,23 +1958,28 @@ function proveAccountMergeWithOAuth(provider, mergeChallenge) {
1511
1958
  code: resp.code,
1512
1959
  redirectUri: redirectUri,
1513
1960
  mergeChallenge: mergeChallenge,
1514
- });
1961
+ }, { silent: true });
1515
1962
  finish(proved);
1516
1963
  } catch (e) {
1517
- showToast((e && e.message) || __t("profile.mergeFailed"), "error");
1518
- finish(null);
1964
+ finish({ ok: false, error: (e && e.message) || __t("profile.mergeFailed") });
1519
1965
  }
1520
1966
  }
1521
1967
  window.addEventListener("message", onMessage);
1968
+ window.addEventListener("storage", onStorage);
1522
1969
  function cancel() {
1523
1970
  try { popup.close(); } catch (e) {}
1524
1971
  finish(null);
1525
1972
  }
1526
1973
  __accountMergeOAuthCleanup = cancel;
1974
+ var existing = readMergeOAuthResultOnce();
1975
+ if (existing) {
1976
+ try { popup.close(); } catch (e6) {}
1977
+ finish(existing);
1978
+ }
1527
1979
  });
1528
1980
  }
1529
1981
 
1530
- async function proveAccountMergeWithEmail(mergeChallenge) {
1982
+ async function proveAccountMergeWithEmail(mergeChallenge, prefillEmail) {
1531
1983
  return new Promise(function(resolve) {
1532
1984
  var d = document.getElementById("email-merge-dialog");
1533
1985
  if (!d) {
@@ -1544,6 +1996,7 @@ async function proveAccountMergeWithEmail(mergeChallenge) {
1544
1996
  + '</div></div>'
1545
1997
  + '<div class="dialog-actions">'
1546
1998
  + '<button type="button" class="btn btn-secondary" id="email-merge-cancel">' + __t("common.cancel") + '</button>'
1999
+ + '<button type="button" class="btn btn-secondary" id="email-merge-switch">' + __t("profile.mergeEmailSwitchMethod") + '</button>'
1547
2000
  + '<button type="button" class="btn btn-secondary" id="email-merge-send">' + __t("profile.mergeEmailSendCode") + '</button>'
1548
2001
  + '<button type="button" class="btn" id="email-merge-ok" disabled>' + __t("common.confirm") + '</button>'
1549
2002
  + '</div>';
@@ -1555,31 +2008,86 @@ async function proveAccountMergeWithEmail(mergeChallenge) {
1555
2008
  var okBtn = document.getElementById("email-merge-ok");
1556
2009
  var verifyId = null;
1557
2010
  var settled = false;
2011
+ var activeChallenge = mergeChallenge;
2012
+ var sentAt = 0;
2013
+ var cooldownTimer = null;
2014
+ var lastError = "";
1558
2015
  function finish(value) {
1559
2016
  if (settled) return;
1560
2017
  settled = true;
2018
+ if (cooldownTimer) clearInterval(cooldownTimer);
2019
+ cooldownTimer = null;
1561
2020
  if (d.open) d.close();
1562
2021
  resolve(value);
1563
2022
  }
2023
+ function secondsLeft() {
2024
+ if (!sentAt) return 0;
2025
+ var left = 60000 - (Date.now() - sentAt);
2026
+ if (left <= 0) return 0;
2027
+ return Math.ceil(left / 1000);
2028
+ }
2029
+ function paintSendButton() {
2030
+ var left = secondsLeft();
2031
+ if (left > 0) {
2032
+ sendBtn.disabled = true;
2033
+ sendBtn.textContent = __t("profile.mergeEmailResendWait", { seconds: String(left) });
2034
+ return;
2035
+ }
2036
+ sendBtn.disabled = false;
2037
+ sendBtn.textContent = sentAt ? __t("profile.mergeEmailResend") : __t("profile.mergeEmailSendCode");
2038
+ }
2039
+ function startCooldown() {
2040
+ if (cooldownTimer) clearInterval(cooldownTimer);
2041
+ paintSendButton();
2042
+ cooldownTimer = setInterval(function() {
2043
+ paintSendButton();
2044
+ if (secondsLeft() <= 0 && cooldownTimer) {
2045
+ clearInterval(cooldownTimer);
2046
+ cooldownTimer = null;
2047
+ }
2048
+ }, 1000);
2049
+ }
2050
+ function markCodeConsumed(message) {
2051
+ codeEl.value = "";
2052
+ codeEl.disabled = true;
2053
+ okBtn.disabled = true;
2054
+ lastError = message || "";
2055
+ statusEl.textContent = lastError
2056
+ ? lastError + " " + __t("profile.mergeEmailRequestNewCode")
2057
+ : __t("profile.mergeEmailRequestNewCode");
2058
+ if (secondsLeft() > 0) startCooldown();
2059
+ else paintSendButton();
2060
+ }
1564
2061
  document.getElementById("email-merge-cancel").onclick = function() { finish(null); };
2062
+ document.getElementById("email-merge-switch").onclick = function() {
2063
+ finish({ switchMethod: true, error: lastError, challenge: activeChallenge });
2064
+ };
1565
2065
  d.onclose = function() { finish(null); };
1566
2066
  sendBtn.onclick = async function() {
1567
2067
  var email = (emailEl.value || "").trim();
1568
2068
  if (!email || email.indexOf("@") === -1) return;
1569
2069
  sendBtn.disabled = true;
1570
2070
  statusEl.textContent = __t("profile.emailSending");
1571
- var sent = await api("POST", "/../email/sendCode", { email: email });
2071
+ var sent = await api("POST", "/../email/sendCode", { email: email }, { silent: true });
1572
2072
  if (!sent || sent.id == null) {
1573
- statusEl.textContent = (sent && sent.error) || __t("profile.emailSendFailed");
1574
- sendBtn.disabled = false;
2073
+ var sendError = (sent && sent.error) || __t("profile.emailSendFailed");
2074
+ statusEl.textContent = sendError;
2075
+ if (emailCodeAlreadySent(sendError)) {
2076
+ if (!sentAt) sentAt = Date.now();
2077
+ startCooldown();
2078
+ } else {
2079
+ sendBtn.disabled = false;
2080
+ }
1575
2081
  return;
1576
2082
  }
1577
2083
  verifyId = sent.id;
2084
+ sentAt = Date.now();
2085
+ codeEl.value = "";
1578
2086
  codeEl.disabled = false;
1579
2087
  okBtn.disabled = false;
1580
2088
  codeEl.focus();
1581
2089
  statusEl.textContent = __t("profile.emailCodeSent");
1582
- sendBtn.disabled = false;
2090
+ startCooldown();
1583
2091
  };
1584
2092
  okBtn.onclick = async function() {
1585
2093
  var code = (codeEl.value || "").trim();
@@ -1588,39 +2096,58 @@ async function proveAccountMergeWithEmail(mergeChallenge) {
1588
2096
  var proved = await api("POST", "/../email/login", {
1589
2097
  code: code,
1590
2098
  id: verifyId,
1591
- mergeChallenge: mergeChallenge,
1592
- });
1593
- if (!proved || !proved.mergeIntent) {
1594
- statusEl.textContent = (proved && proved.error) || __t("profile.mergeFailed");
1595
- okBtn.disabled = false;
2099
+ mergeChallenge: activeChallenge,
2100
+ }, { silent: true });
2101
+ if (proved && proved.mergeIntent) {
2102
+ finish(proved);
1596
2103
  return;
1597
2104
  }
1598
- finish(proved);
2105
+ var err = (proved && proved.error) || __t("profile.mergeFailed");
2106
+ if (mergeChallengeLooksExpired(err)) {
2107
+ var restarted = await api("POST", "/../account/merge/start", {}, { silent: true });
2108
+ if (restarted && restarted.challenge) activeChallenge = restarted.challenge;
2109
+ }
2110
+ markCodeConsumed(err);
1599
2111
  };
2112
+ if (typeof prefillEmail === "string" && prefillEmail) emailEl.value = prefillEmail;
1600
2113
  d.showModal();
1601
2114
  emailEl.focus();
1602
2115
  });
1603
2116
  }
1604
2117
 
1605
- async function finalizeAccountMerge(proved) {
2118
+ async function finalizeAccountMerge(proved, candidate) {
1606
2119
  if (!proved || !proved.mergeIntent) {
1607
2120
  showToast((proved && proved.error) || __t("profile.mergeFailed"), "error");
1608
2121
  return false;
1609
2122
  }
1610
2123
  var plan = await api("POST", "/../account/merge/dry-run", { mergeIntent: proved.mergeIntent });
1611
- var ok = await confirmMergePlan(plan);
1612
- if (!ok) {
2124
+ if (!plan || plan.ok === false || !plan.accounts) return false;
2125
+ // Server omits keep → session. The first paint must match recommendedKeep.
2126
+ if ((plan.recommendedKeep === "session" || plan.recommendedKeep === "proved") && plan.keep !== plan.recommendedKeep) {
2127
+ var next = plan.recommendedKeep;
2128
+ var aligned = await api("POST", "/../account/merge/dry-run", { mergeIntent: proved.mergeIntent, keep: next }, { silent: true });
2129
+ if (aligned && aligned.ok !== false && aligned.accounts) plan = aligned;
2130
+ else showToast((aligned && aligned.error) || __t("profile.mergeFailed"), "error");
2131
+ }
2132
+ var candidateEmail = candidate && typeof candidate.email === "string" ? candidate.email : "";
2133
+ plan.__hasCandidate = !!(candidate && typeof candidate === "object");
2134
+ var confirmed = await confirmMergePlan(plan, proved.mergeIntent, candidateEmail);
2135
+ if (!confirmed) {
1613
2136
  showToast(__t("profile.mergeCancelled"), "error");
1614
2137
  return false;
1615
2138
  }
1616
- var committed = await api("POST", "/../account/merge/commit", { mergeIntent: proved.mergeIntent });
1617
- if (committed && committed.ok) {
1618
- showToast(__t("profile.mergeSuccess"), "success");
1619
- loadConnectedAccounts();
1620
- return true;
1621
- }
1622
- showToast((committed && committed.error) || __t("profile.mergeFailed"), "error");
1623
- return false;
2139
+ return true;
2140
+ }
2141
+
2142
+ function mergeCandidateWhenLabel(iso) {
2143
+ var lang = (document.documentElement && document.documentElement.lang) || "en";
2144
+ return formatMergeCandidateWhen(iso, Date.now(), lang, __t);
2145
+ }
2146
+
2147
+ function mergePromptIcon(provider) {
2148
+ if (provider && __providerIcons[provider]) return getProviderIconSvg(provider);
2149
+ var ch = provider ? provider.charAt(0).toUpperCase() : "?";
2150
+ return '<span class="ca-merge-prompt-fallback">' + escapeHtml(ch) + '</span>';
1624
2151
  }
1625
2152
 
1626
2153
  function hideMergePrompt() {
@@ -1631,10 +2158,48 @@ function hideMergePrompt() {
1631
2158
  el.innerHTML = "";
1632
2159
  }
1633
2160
 
1634
- function dismissMergePrompt() {
2161
+ function paintMergePrompt(rows, hiddenProviders) {
2162
+ var el = document.getElementById("merge-candidate-prompt");
2163
+ if (!el) return;
2164
+ if (!rows || !rows.length) {
2165
+ hideMergePrompt();
2166
+ return;
2167
+ }
2168
+ el.innerHTML = renderMergePromptHtml(rows[0].email || "", rows, __t, escapeHtml, mergePromptIcon, getProviderLabel, mergeCandidateWhenLabel, hiddenProviders || __hiddenProviders);
2169
+ el.hidden = false;
2170
+ el.style.display = "";
2171
+ }
2172
+
2173
+ function startMergeCandidate(btn) {
2174
+ var ref = btn && btn.getAttribute ? btn.getAttribute("data-merge-ref") : "";
2175
+ var list = __mergePromptCandidates || [];
2176
+ for (var i = 0; i < list.length; i++) {
2177
+ if (list[i].ref === ref) {
2178
+ startAccountMerge(list[i]);
2179
+ return;
2180
+ }
2181
+ }
2182
+ }
2183
+
2184
+ function hideMergeCandidate(btn) {
2185
+ var ref = btn && btn.getAttribute ? btn.getAttribute("data-merge-ref") : "";
2186
+ var did = __mergePromptDid || "";
2187
+ if (did && ref) {
2188
+ try { localStorage["connect.mergePrompt.notMine." + did + "." + ref] = "1"; } catch (e) {}
2189
+ }
2190
+ var next = [];
2191
+ var list = __mergePromptCandidates || [];
2192
+ for (var i = 0; i < list.length; i++) {
2193
+ if (list[i].ref !== ref) next.push(list[i]);
2194
+ }
2195
+ __mergePromptCandidates = next;
2196
+ paintMergePrompt(next, __hiddenProviders);
2197
+ }
2198
+
2199
+ function dismissMergePromptLater() {
1635
2200
  var did = __mergePromptDid || "";
1636
2201
  if (did) {
1637
- try { localStorage["connect.mergePrompt.dismissed." + did] = "1"; } catch (e) {}
2202
+ try { sessionStorage["connect.mergePrompt.later." + did] = "1"; } catch (e) {}
1638
2203
  }
1639
2204
  hideMergePrompt();
1640
2205
  }
@@ -1642,74 +2207,117 @@ function dismissMergePrompt() {
1642
2207
  async function maybeShowMergePrompt(did) {
1643
2208
  hideMergePrompt();
1644
2209
  __mergePromptDid = did || "";
2210
+ __mergePromptCandidates = [];
1645
2211
  if (!did) return;
1646
2212
  try {
1647
- if (localStorage["connect.mergePrompt.dismissed." + did] === "1") return;
2213
+ if (sessionStorage["connect.mergePrompt.later." + did] === "1") return;
1648
2214
  } catch (e) {}
1649
2215
  var data = await api("GET", "/../account/merge/candidate", undefined, { silent: true });
1650
- if (!data || data.mergeCandidate !== true) return;
2216
+ if (!data || data.mergeCandidate !== true || !data.candidates || !data.candidates.length) return;
1651
2217
  try {
1652
- if (localStorage["connect.mergePrompt.dismissed." + did] === "1") return;
1653
- } catch (e) {}
2218
+ if (sessionStorage["connect.mergePrompt.later." + did] === "1") return;
2219
+ } catch (e2) {}
2220
+ var email = typeof data.email === "string" ? data.email : "";
2221
+ if (!email) return;
2222
+ var visible = [];
2223
+ for (var i = 0; i < data.candidates.length && visible.length < 5; i++) {
2224
+ var c = data.candidates[i];
2225
+ if (!c || typeof c.ref !== "string" || !c.ref) continue;
2226
+ try {
2227
+ if (localStorage["connect.mergePrompt.notMine." + did + "." + c.ref] === "1") continue;
2228
+ } catch (e3) {}
2229
+ visible.push({
2230
+ ref: c.ref,
2231
+ methods: Array.isArray(c.methods) ? c.methods : [],
2232
+ emailLogin: c.emailLogin === true,
2233
+ email: email,
2234
+ createdAt: typeof c.createdAt === "string" ? c.createdAt : "",
2235
+ lastLoginAt: typeof c.lastLoginAt === "string" ? c.lastLoginAt : "",
2236
+ });
2237
+ }
2238
+ if (!visible.length) return;
1654
2239
  var el = document.getElementById("merge-candidate-prompt");
1655
2240
  if (!el) return;
1656
- el.innerHTML = '<div class="ca-merge-prompt-title">' + escapeHtml(__t("profile.mergePromptTitle")) + '</div>'
1657
- + '<p class="ca-merge-prompt-body">' + escapeHtml(__t("profile.mergePromptBody")) + '</p>'
1658
- + '<div class="ca-merge-prompt-actions">'
1659
- + '<button type="button" class="btn" id="merge-candidate-prompt-action" onclick="startAccountMerge()">' + escapeHtml(__t("profile.mergePromptAction")) + '</button>'
1660
- + '<button type="button" class="btn btn-secondary" id="merge-candidate-prompt-dismiss" onclick="dismissMergePrompt()">' + escapeHtml(__t("profile.mergePromptDismiss")) + '</button>'
1661
- + '</div>';
1662
- el.hidden = false;
1663
- el.style.display = "";
2241
+ __mergePromptCandidates = visible;
2242
+ paintMergePrompt(visible, __hiddenProviders);
1664
2243
  }
1665
2244
 
1666
- async function startAccountMerge() {
2245
+ async function startAccountMerge(candidate) {
1667
2246
  cleanupAccountMergeOAuth();
1668
- var start;
1669
- try {
1670
- start = await api("POST", "/../account/merge/start", {});
1671
- } catch (e) {
1672
- showToast((e && e.message) || __t("profile.mergeFailed"), "error");
1673
- return;
2247
+ var guided = null;
2248
+ if (candidate && typeof candidate.ref === "string" && candidate.ref) {
2249
+ guided = partitionMergeMethods(candidate, listMergeMethods(), __hiddenProviders);
1674
2250
  }
1675
- if (!start || !start.challenge) {
1676
- showToast(__t("profile.mergeFailed"), "error");
1677
- return;
1678
- }
1679
-
1680
2251
  var d = document.getElementById("account-merge-method-dialog");
1681
2252
  if (!d) {
1682
2253
  d = document.createElement("dialog");
1683
2254
  d.id = "account-merge-method-dialog";
1684
2255
  document.body.appendChild(d);
1685
2256
  }
1686
- d.innerHTML = renderMergeMethodPickerHtml(listMergeMethods(), __t, escapeHtml, getProviderIconSvg, getProviderLabel);
1687
- document.getElementById("account-merge-method-cancel").onclick = function() { d.close(); };
1688
- var buttons = d.querySelectorAll("[data-merge-method]");
1689
- for (var b = 0; b < buttons.length; b++) {
1690
- buttons[b].onclick = async function(event) {
1691
- var button = event.currentTarget;
1692
- var method = button.getAttribute("data-merge-method");
1693
- var selectedProvider = button.getAttribute("data-merge-provider");
1694
- d.close();
1695
- try {
1696
- var proved = null;
1697
- if (method === "wallet") proved = await proveAccountMergeWithWallet(start.challenge);
1698
- else if (method === "passkey") proved = await proveAccountMergeWithPasskey(start.challenge);
1699
- else if (method === "oauth") proved = await proveAccountMergeWithOAuth(selectedProvider, start.challenge);
1700
- else if (method === "email") proved = await proveAccountMergeWithEmail(start.challenge);
1701
- if (proved) await finalizeAccountMerge(proved);
1702
- } catch (e) {
1703
- if (!e || e.name !== "NotAllowedError") {
1704
- showToast((e && e.message) || __t("profile.mergeFailed"), "error");
2257
+ var challenge = null;
2258
+ function paint(errorText) {
2259
+ d.innerHTML = renderMergeMethodPickerHtml(guided ? [] : listMergeMethods(), __t, escapeHtml, getProviderIconSvg, getProviderLabel, errorText || "", guided);
2260
+ var cancel = document.getElementById("account-merge-method-cancel");
2261
+ if (cancel) cancel.onclick = function() { d.close(); };
2262
+ var buttons = d.querySelectorAll("[data-merge-method]");
2263
+ for (var b = 0; b < buttons.length; b++) {
2264
+ buttons[b].onclick = onPick;
2265
+ }
2266
+ }
2267
+ paint("");
2268
+ d.showModal();
2269
+
2270
+ async function ensureChallenge(force) {
2271
+ if (challenge && !force) return challenge;
2272
+ var start = await api("POST", "/../account/merge/start", {}, { silent: true });
2273
+ if (!start || !start.challenge) {
2274
+ paint((start && start.error) || __t("profile.mergeFailed"));
2275
+ return null;
2276
+ }
2277
+ challenge = start.challenge;
2278
+ return challenge;
2279
+ }
2280
+
2281
+ async function onPick(event) {
2282
+ var button = event.currentTarget;
2283
+ var method = button.getAttribute("data-merge-method");
2284
+ var selectedProvider = button.getAttribute("data-merge-provider");
2285
+ paint("");
2286
+ var active = await ensureChallenge(false);
2287
+ if (!active) return;
2288
+ try {
2289
+ var proved = null;
2290
+ if (method === "wallet") proved = await proveAccountMergeWithWallet(active);
2291
+ else if (method === "passkey") proved = await proveAccountMergeWithPasskey(active);
2292
+ else if (method === "oauth") proved = await proveAccountMergeWithOAuth(selectedProvider, active);
2293
+ else if (method === "email") proved = await proveAccountMergeWithEmail(active, button.getAttribute("data-merge-email") || "");
2294
+ if (proved && proved.switchMethod) {
2295
+ if (typeof proved.challenge === "string" && proved.challenge) challenge = proved.challenge;
2296
+ paint(proved.error || "");
2297
+ return;
2298
+ }
2299
+ if (proved && proved.mergeIntent) {
2300
+ if (d.open) d.close();
2301
+ await finalizeAccountMerge(proved, candidate);
2302
+ return;
2303
+ }
2304
+ if (proved && proved.error) {
2305
+ if (mergeChallengeLooksExpired(proved.error)) {
2306
+ challenge = null;
2307
+ await ensureChallenge(true);
1705
2308
  }
2309
+ paint(proved.error);
2310
+ return;
1706
2311
  }
1707
- };
2312
+ } catch (e) {
2313
+ if (!e || e.name !== "NotAllowedError") {
2314
+ paint((e && e.message) || __t("profile.mergeFailed"));
2315
+ }
2316
+ }
1708
2317
  }
1709
- d.showModal();
1710
2318
  }
1711
2319
 
1712
- function confirmMergePlan(plan) {
2320
+ function confirmMergePlan(plan, mergeIntent, candidateEmail) {
1713
2321
  return new Promise(function(resolve) {
1714
2322
  var d = document.getElementById("merge-confirm-dialog");
1715
2323
  if (!d) {
@@ -1718,32 +2326,193 @@ function confirmMergePlan(plan) {
1718
2326
  d.className = "merge-confirm-dialog";
1719
2327
  document.body.appendChild(d);
1720
2328
  }
1721
- d.innerHTML = '<div class="dialog-content">'
1722
- + '<h3>' + escapeHtml(__t("profile.mergeConfirmTitle")) + '</h3>'
1723
- + renderMergeConfirmBodyHtml(plan, __t, escapeHtml, getProviderLabel)
1724
- + '</div>'
1725
- + '<div class="dialog-actions">'
1726
- + '<button class="btn btn-secondary" id="merge-confirm-cancel">' + escapeHtml(__t("common.cancel")) + '</button>'
1727
- + '<button class="btn btn-danger" id="merge-confirm-ok">' + escapeHtml(__t("common.confirm")) + '</button>'
1728
- + '</div>';
2329
+ var current = plan || {};
2330
+ var selectedKeep = current.keep === "proved" || current.keep === "session" ? current.keep : "session";
2331
+ // accounts.session.did stays the signed-in DID. intoDid follows keep.
2332
+ var sessionAccount = current.accounts && current.accounts.session;
2333
+ var sessionDid = sessionAccount && typeof sessionAccount.did === "string" ? sessionAccount.did : "";
2334
+ var hasCandidate = current.__hasCandidate === true;
2335
+ var loading = false;
2336
+ var generation = 0;
2337
+ var phase = "preview";
2338
+ var settled = false;
2339
+ var resultPayload = null;
1729
2340
 
1730
- var okBtn = document.getElementById("merge-confirm-ok");
1731
- var cancelBtn = document.getElementById("merge-confirm-cancel");
1732
- if (plan && plan.tooLarge) okBtn.disabled = true;
2341
+ function keptDid() {
2342
+ var side = selectedKeep === "proved" ? current.accounts && current.accounts.proved : current.accounts && current.accounts.session;
2343
+ if (side && typeof side.did === "string" && side.did) return side.did;
2344
+ return typeof current.intoDid === "string" ? current.intoDid : "";
2345
+ }
2346
+
2347
+ function submitLabel() {
2348
+ if (selectedKeep === "session") return __t("profile.mergeConfirmSubmitKeep");
2349
+ return __t("profile.mergeConfirmSubmitSwitch", { did: truncateDid(keptDid()) });
2350
+ }
2351
+
2352
+ function paint() {
2353
+ d.innerHTML = '<div class="dialog-content">'
2354
+ + '<h3 id="merge-confirm-title">' + escapeHtml(__t("profile.mergeConfirmTitle")) + '</h3>'
2355
+ + renderMergeConfirmBodyHtml(current, __t, escapeHtml, getProviderLabel, {
2356
+ selectedKeep: selectedKeep,
2357
+ hasCandidate: hasCandidate,
2358
+ candidateEmail: candidateEmail || "",
2359
+ shortDid: truncateDid,
2360
+ loading: loading
2361
+ })
2362
+ + '</div>'
2363
+ + '<div class="dialog-actions">'
2364
+ + '<button type="button" class="btn btn-secondary" id="merge-confirm-cancel">' + escapeHtml(__t("common.cancel")) + '</button>'
2365
+ + '<button type="button" class="btn btn-danger" id="merge-confirm-ok">' + escapeHtml(submitLabel()) + '</button>'
2366
+ + '</div>';
2367
+ var okBtn = document.getElementById("merge-confirm-ok");
2368
+ var cancelBtn = document.getElementById("merge-confirm-cancel");
2369
+ if (loading || current.tooLarge || phase !== "preview") okBtn.disabled = true;
2370
+ cancelBtn.onclick = function() { finish(false); };
2371
+ okBtn.onclick = onOk;
2372
+ var radios = d.querySelectorAll('input[name="merge-keep"]');
2373
+ for (var i = 0; i < radios.length; i++) radios[i].onchange = onKeepChange;
2374
+ var copies = d.querySelectorAll("[data-copy-did]");
2375
+ for (var c = 0; c < copies.length; c++) copies[c].onclick = onCopy;
2376
+ }
2377
+
2378
+ function onCopy(event) {
2379
+ if (event) {
2380
+ if (event.preventDefault) event.preventDefault();
2381
+ if (event.stopPropagation) event.stopPropagation();
2382
+ }
2383
+ var btn = event && event.currentTarget;
2384
+ var full = btn && btn.getAttribute ? btn.getAttribute("data-copy-did") : "";
2385
+ if (full) copyToClipboard(full);
2386
+ }
2387
+
2388
+ function onKeepChange(event) {
2389
+ if (phase !== "preview") return;
2390
+ var input = event && event.currentTarget;
2391
+ var next = input && input.value;
2392
+ if (next !== "session" && next !== "proved") return;
2393
+ if (next === selectedKeep) return;
2394
+ selectedKeep = next;
2395
+ loading = true;
2396
+ var token = ++generation;
2397
+ var okBtn = document.getElementById("merge-confirm-ok");
2398
+ if (okBtn) okBtn.disabled = true;
2399
+ setTimeout(function() {
2400
+ if (token !== generation || !loading || phase !== "preview") return;
2401
+ paint();
2402
+ }, 0);
2403
+ refreshKeep(next, token);
2404
+ }
2405
+
2406
+ async function refreshKeep(next, token) {
2407
+ var refreshed = await api("POST", "/../account/merge/dry-run", { mergeIntent: mergeIntent, keep: next }, { silent: true });
2408
+ if (token !== generation) return;
2409
+ loading = false;
2410
+ if (!refreshed || refreshed.ok === false || !refreshed.accounts) {
2411
+ showToast((refreshed && refreshed.error) || __t("profile.mergeFailed"), "error");
2412
+ selectedKeep = current.keep === "proved" || current.keep === "session" ? current.keep : "session";
2413
+ paint();
2414
+ return;
2415
+ }
2416
+ current = refreshed;
2417
+ selectedKeep = current.keep === "proved" || current.keep === "session" ? current.keep : next;
2418
+ paint();
2419
+ }
2420
+
2421
+ async function onOk() {
2422
+ if (phase !== "preview" || loading || current.tooLarge) return;
2423
+ var okBtn = document.getElementById("merge-confirm-ok");
2424
+ if (!okBtn || okBtn.disabled) return;
2425
+ okBtn.disabled = true;
2426
+ phase = "committing";
2427
+ generation += 1;
2428
+ var committed = await api("POST", "/../account/merge/commit", { mergeIntent: mergeIntent, keep: selectedKeep });
2429
+ if (settled) return;
2430
+ if (!committed || committed.ok === false || !committed.survivorDid) {
2431
+ phase = "preview";
2432
+ if (!d.open) {
2433
+ try { d.showModal(); } catch (err) {}
2434
+ }
2435
+ paint();
2436
+ return;
2437
+ }
2438
+ phase = "done";
2439
+ resultPayload = committed;
2440
+ showDone(committed);
2441
+ }
2442
+
2443
+ function showDone(committed) {
2444
+ var survivorDid = committed && typeof committed.survivorDid === "string" ? committed.survivorDid : "";
2445
+ d.innerHTML = '<div class="dialog-content">'
2446
+ + '<h3 id="merge-confirm-title">' + escapeHtml(__t("profile.mergeDoneTitle")) + '</h3>'
2447
+ + renderMergeDoneBodyHtml({
2448
+ survivorDid: survivorDid,
2449
+ survivorMethods: current.survivorMethods
2450
+ }, __t, escapeHtml, getProviderLabel, truncateDid)
2451
+ + '</div>'
2452
+ + '<div class="dialog-actions">'
2453
+ + '<button type="button" class="btn" id="merge-confirm-finish">' + escapeHtml(__t("profile.mergeDoneFinish")) + '</button>'
2454
+ + '</div>';
2455
+ var finishBtn = document.getElementById("merge-confirm-finish");
2456
+ finishBtn.onclick = function() { finishDone(survivorDid); };
2457
+ if (!d.open) {
2458
+ try { d.showModal(); } catch (err) {}
2459
+ }
2460
+ }
2461
+
2462
+ function finishDone(survivorDid) {
2463
+ if (settled) return;
2464
+ settled = true;
2465
+ d.oncancel = null;
2466
+ d.onclose = null;
2467
+ if (d.open) d.close();
2468
+ if (survivorDid && sessionDid && survivorDid !== sessionDid) location.reload();
2469
+ else loadConnectedAccounts();
2470
+ resolve(true);
2471
+ }
1733
2472
 
1734
- var settled = false;
1735
2473
  function finish(result) {
1736
2474
  if (settled) return;
2475
+ if (phase === "done") {
2476
+ finishDone(resultPayload && resultPayload.survivorDid ? resultPayload.survivorDid : "");
2477
+ return;
2478
+ }
2479
+ if (phase === "committing") {
2480
+ if (!d.open) {
2481
+ try { d.showModal(); } catch (err) {}
2482
+ }
2483
+ return;
2484
+ }
1737
2485
  settled = true;
1738
- okBtn.onclick = null;
1739
- cancelBtn.onclick = null;
2486
+ d.oncancel = null;
1740
2487
  d.onclose = null;
1741
- d.close();
2488
+ if (d.open) d.close();
1742
2489
  resolve(result);
1743
2490
  }
1744
- okBtn.onclick = function() { if (okBtn.disabled) return; finish(true); };
1745
- cancelBtn.onclick = function() { finish(false); };
1746
- d.onclose = function() { finish(false); };
2491
+
2492
+ d.oncancel = function(event) {
2493
+ if (event && event.preventDefault) event.preventDefault();
2494
+ if (phase === "committing") return;
2495
+ if (phase === "done") {
2496
+ finishDone(resultPayload && resultPayload.survivorDid ? resultPayload.survivorDid : "");
2497
+ return;
2498
+ }
2499
+ finish(false);
2500
+ };
2501
+ d.onclose = function() {
2502
+ if (settled) return;
2503
+ if (phase === "committing") {
2504
+ if (!d.open) {
2505
+ try { d.showModal(); } catch (err) {}
2506
+ }
2507
+ return;
2508
+ }
2509
+ if (phase === "done") {
2510
+ finishDone(resultPayload && resultPayload.survivorDid ? resultPayload.survivorDid : "");
2511
+ return;
2512
+ }
2513
+ finish(false);
2514
+ };
2515
+ paint();
1747
2516
  d.showModal();
1748
2517
  });
1749
2518
  }