@arcblock/did-connect-core 4.1.53 → 4.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ui/i18n.d.ts.map +1 -1
- package/dist/ui/i18n.js +32 -4
- package/dist/ui/i18n.js.map +1 -1
- package/dist/ui/index.bundle.js +321 -158
- package/dist/ui/login-dialog.d.ts.map +1 -1
- package/dist/ui/login-dialog.js +4 -0
- package/dist/ui/login-dialog.js.map +1 -1
- package/dist/ui/login-page.d.ts +4 -0
- package/dist/ui/login-page.d.ts.map +1 -1
- package/dist/ui/login-page.js +232 -147
- package/dist/ui/login-page.js.map +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/styles.js +54 -23
- package/dist/ui/styles.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/login-page.js
CHANGED
|
@@ -268,6 +268,8 @@ export class LoginPage {
|
|
|
268
268
|
}
|
|
269
269
|
/** Locale sits on the card's top-right row, level with Back / Cancel. */
|
|
270
270
|
renderCardTools(card) {
|
|
271
|
+
if (!(this.options.showLocaleSelector ?? this.options.mode !== 'dialog'))
|
|
272
|
+
return;
|
|
271
273
|
const tools = document.createElement('div');
|
|
272
274
|
tools.className = 'card-tools';
|
|
273
275
|
this.renderLocaleSwitcher(tools);
|
|
@@ -325,6 +327,8 @@ export class LoginPage {
|
|
|
325
327
|
trigger.addEventListener('click', (event) => {
|
|
326
328
|
event.stopPropagation();
|
|
327
329
|
setOpen(menu.hidden);
|
|
330
|
+
if (!menu.hidden)
|
|
331
|
+
menu.querySelector('[aria-checked="true"]')?.focus();
|
|
328
332
|
});
|
|
329
333
|
for (const item of locales) {
|
|
330
334
|
const checked = item.code === current.code;
|
|
@@ -334,12 +338,13 @@ export class LoginPage {
|
|
|
334
338
|
btn.setAttribute('role', 'menuitemradio');
|
|
335
339
|
btn.setAttribute('aria-checked', checked ? 'true' : 'false');
|
|
336
340
|
btn.dataset.locale = item.code;
|
|
341
|
+
btn.tabIndex = -1;
|
|
337
342
|
btn.title = item.label;
|
|
338
|
-
|
|
343
|
+
{
|
|
339
344
|
const mark = document.createElement('span');
|
|
340
345
|
mark.className = 'locale-check';
|
|
341
346
|
mark.setAttribute('aria-hidden', 'true');
|
|
342
|
-
mark.textContent = '✓';
|
|
347
|
+
mark.textContent = checked ? '✓' : '';
|
|
343
348
|
btn.appendChild(mark);
|
|
344
349
|
}
|
|
345
350
|
const label = document.createElement('span');
|
|
@@ -351,6 +356,25 @@ export class LoginPage {
|
|
|
351
356
|
});
|
|
352
357
|
menu.appendChild(btn);
|
|
353
358
|
}
|
|
359
|
+
wrapper.addEventListener('keydown', (event) => {
|
|
360
|
+
const items = [...menu.querySelectorAll('.locale-btn')];
|
|
361
|
+
const active = this.shadow?.activeElement ?? document.activeElement;
|
|
362
|
+
const index = items.indexOf(active);
|
|
363
|
+
if (event.key === 'Escape') {
|
|
364
|
+
event.preventDefault();
|
|
365
|
+
event.stopPropagation();
|
|
366
|
+
setOpen(false);
|
|
367
|
+
trigger.focus();
|
|
368
|
+
}
|
|
369
|
+
else if (event.key === 'Tab')
|
|
370
|
+
setOpen(false);
|
|
371
|
+
else if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
|
|
372
|
+
event.preventDefault();
|
|
373
|
+
setOpen(true);
|
|
374
|
+
const next = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 : index < 0 ? (event.key === 'ArrowUp' ? items.length - 1 : 0) : (index + (event.key === 'ArrowUp' ? -1 : 1) + items.length) % items.length;
|
|
375
|
+
items[next]?.focus();
|
|
376
|
+
}
|
|
377
|
+
});
|
|
354
378
|
const onOutsideClick = (event) => {
|
|
355
379
|
const path = event.composedPath();
|
|
356
380
|
if (path.includes(wrapper))
|
|
@@ -406,6 +430,21 @@ export class LoginPage {
|
|
|
406
430
|
break;
|
|
407
431
|
}
|
|
408
432
|
}
|
|
433
|
+
/** Keep recognition next to the action, without a second page subtitle. */
|
|
434
|
+
markLastUsed(button, method, locale) {
|
|
435
|
+
if (!this.options.preferenceKey)
|
|
436
|
+
return;
|
|
437
|
+
try {
|
|
438
|
+
if (localStorage.getItem(`did-login-method:${this.options.preferenceKey}`) !== method)
|
|
439
|
+
return;
|
|
440
|
+
const hint = document.createElement('span');
|
|
441
|
+
hint.className = 'last-used';
|
|
442
|
+
hint.textContent = t('lastUsed', locale);
|
|
443
|
+
button.classList.add('has-last-used');
|
|
444
|
+
button.appendChild(hint);
|
|
445
|
+
}
|
|
446
|
+
catch { /* Storage may be unavailable in private mode. */ }
|
|
447
|
+
}
|
|
409
448
|
renderMethodsView(card, locale, methods) {
|
|
410
449
|
// ── Escape hatch ("Cancel") — top-left, mirrors the locale switcher
|
|
411
450
|
// (top-right). Standard placement for a standalone auth card. ──
|
|
@@ -413,21 +452,6 @@ export class LoginPage {
|
|
|
413
452
|
this.renderHomeLink(card, locale);
|
|
414
453
|
// ── Brand header ──
|
|
415
454
|
this.renderBrandHeader(card, locale);
|
|
416
|
-
if (this.options.preferenceKey) {
|
|
417
|
-
try {
|
|
418
|
-
const last = localStorage.getItem(`did-login-method:${this.options.preferenceKey}`);
|
|
419
|
-
const labels = { passkey: 'Passkey', oauth: 'OAuth', email: t('emailButton', locale), 'did-connect': 'DID Wallet' };
|
|
420
|
-
const provider = last?.startsWith('oauth:') ? this.options.oauthProviders?.find(p => p.enabled && `oauth:${p.id}` === last) : undefined;
|
|
421
|
-
const label = provider?.name ?? (last ? labels[last] : undefined);
|
|
422
|
-
if (last && label && (methods.includes(last) || (provider && methods.includes('oauth')))) {
|
|
423
|
-
const hint = document.createElement('p');
|
|
424
|
-
hint.className = 'subtitle';
|
|
425
|
-
hint.textContent = t('lastUsed', locale, { method: label });
|
|
426
|
-
card.appendChild(hint);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
catch { /* private mode */ }
|
|
430
|
-
}
|
|
431
455
|
// Passkey unsupported warning
|
|
432
456
|
const requestedPasskey = this.options.methods.includes('passkey');
|
|
433
457
|
if (requestedPasskey && !methods.includes('passkey')) {
|
|
@@ -721,11 +745,12 @@ export class LoginPage {
|
|
|
721
745
|
}
|
|
722
746
|
});
|
|
723
747
|
}
|
|
724
|
-
renderBackButton(parent, locale) {
|
|
748
|
+
renderBackButton(parent, locale, onBack = () => this.switchView('methods')) {
|
|
725
749
|
const btn = document.createElement('button');
|
|
750
|
+
btn.type = 'button';
|
|
726
751
|
btn.className = 'back-btn';
|
|
727
752
|
btn.innerHTML = `${ICON_BACK_ARROW} ${escapeHtml(t('back', locale))}`;
|
|
728
|
-
btn.addEventListener('click',
|
|
753
|
+
btn.addEventListener('click', onBack);
|
|
729
754
|
parent.appendChild(btn);
|
|
730
755
|
}
|
|
731
756
|
/**
|
|
@@ -838,12 +863,6 @@ export class LoginPage {
|
|
|
838
863
|
}
|
|
839
864
|
footer.appendChild(links);
|
|
840
865
|
}
|
|
841
|
-
if (this.currentView !== 'methods' && this.options.mode !== 'dialog') {
|
|
842
|
-
this.renderHomeLink(footer, locale);
|
|
843
|
-
const cancel = footer.querySelector('.home-link');
|
|
844
|
-
if (cancel)
|
|
845
|
-
cancel.style.cssText = 'position:static;display:inline-flex;margin-top:12px';
|
|
846
|
-
}
|
|
847
866
|
parent.appendChild(footer);
|
|
848
867
|
}
|
|
849
868
|
renderDivider(parent, locale) {
|
|
@@ -943,11 +962,8 @@ export class LoginPage {
|
|
|
943
962
|
createBtn.id = 'passkey-btn';
|
|
944
963
|
createBtn.innerHTML = `${ICON_PASSKEY} ${escapeHtml(t('passkeyCreate', locale))}`;
|
|
945
964
|
section.appendChild(createBtn);
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
backLink.className = 'passkey-alt';
|
|
949
|
-
backLink.textContent = t('passkeyBackToSignIn', locale);
|
|
950
|
-
section.appendChild(backLink);
|
|
965
|
+
parent.querySelector('.back-btn')?.remove();
|
|
966
|
+
this.renderBackButton(parent, locale, () => renderSignInState());
|
|
951
967
|
const doCreate = async () => {
|
|
952
968
|
if (createBtn.disabled)
|
|
953
969
|
return; // Enter-key auto-repeat on the name input
|
|
@@ -976,7 +992,6 @@ export class LoginPage {
|
|
|
976
992
|
createBtn.addEventListener('click', doCreate);
|
|
977
993
|
nameInput.addEventListener('keydown', (e) => { if (e.key === 'Enter')
|
|
978
994
|
doCreate(); });
|
|
979
|
-
backLink.addEventListener('click', () => renderSignInState());
|
|
980
995
|
createBtn.focus();
|
|
981
996
|
};
|
|
982
997
|
// ── State 0: sign-in (default) ──
|
|
@@ -1015,6 +1030,7 @@ export class LoginPage {
|
|
|
1015
1030
|
};
|
|
1016
1031
|
const renderSignInState = () => {
|
|
1017
1032
|
this.cardEl?.classList.remove('passkey-creating');
|
|
1033
|
+
parent.querySelector('.back-btn')?.remove();
|
|
1018
1034
|
this.abort?.abort();
|
|
1019
1035
|
this.generation++;
|
|
1020
1036
|
const heading = this.cardEl?.querySelector('h1');
|
|
@@ -1026,6 +1042,7 @@ export class LoginPage {
|
|
|
1026
1042
|
signInBtn.className = this.cardEl?.classList.contains('social-first') ? 'btn btn-secondary' : 'btn';
|
|
1027
1043
|
signInBtn.id = 'passkey-btn';
|
|
1028
1044
|
signInBtn.innerHTML = `${ICON_PASSKEY} ${escapeHtml(t('passkeySignIn', locale))}`;
|
|
1045
|
+
this.markLastUsed(signInBtn, 'passkey', locale);
|
|
1029
1046
|
section.appendChild(signInBtn);
|
|
1030
1047
|
signInBtn.addEventListener('click', () => doSignIn());
|
|
1031
1048
|
const createLink = document.createElement('button');
|
|
@@ -1049,172 +1066,235 @@ export class LoginPage {
|
|
|
1049
1066
|
startOAuthPopup(providerId, btn) {
|
|
1050
1067
|
this.oauthCleanup?.();
|
|
1051
1068
|
const epoch = this.generation;
|
|
1069
|
+
const locale = this.options.locale;
|
|
1052
1070
|
const servicePrefix = this.config.servicePrefix ?? '/.well-known/service';
|
|
1053
|
-
// Federation: popup opens Master's OAuth login URL (cross-domain callback)
|
|
1054
1071
|
const oauthBase = this.options.masterOAuthOrigin
|
|
1055
|
-
? `${this.options.masterOAuthOrigin}${servicePrefix}`
|
|
1056
|
-
: servicePrefix;
|
|
1072
|
+
? `${this.options.masterOAuthOrigin}${servicePrefix}` : servicePrefix;
|
|
1057
1073
|
const oauthOrigin = new URL(oauthBase, window.location.href).origin;
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
return;
|
|
1074
|
+
let returnUrl = window.location.href;
|
|
1075
|
+
if (this.options.mode !== 'dialog' && window.location.pathname === `${servicePrefix}/login`) {
|
|
1076
|
+
const requested = new URLSearchParams(window.location.search).get('return_to');
|
|
1077
|
+
try {
|
|
1078
|
+
const target = new URL(requested || '/', window.location.origin);
|
|
1079
|
+
returnUrl = target.origin === window.location.origin && !/^\/[/\\]/.test(target.pathname) ? target.href : `${window.location.origin}/`;
|
|
1080
|
+
}
|
|
1081
|
+
catch {
|
|
1082
|
+
returnUrl = `${window.location.origin}/`;
|
|
1083
|
+
}
|
|
1069
1084
|
}
|
|
1070
|
-
|
|
1085
|
+
const loginUrl = `${oauthBase}/api/oauth/${encodeURIComponent(providerId)}/login?returnUrl=${encodeURIComponent(returnUrl)}`;
|
|
1086
|
+
const provider = this.options.oauthProviders?.find(p => p.id === providerId)?.name ?? providerId;
|
|
1071
1087
|
const originalHTML = btn.innerHTML;
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1088
|
+
let popup = null;
|
|
1089
|
+
let settled = false;
|
|
1090
|
+
let exchanging = false;
|
|
1091
|
+
let probing = false;
|
|
1092
|
+
let pollTimer;
|
|
1093
|
+
let sessionTimer;
|
|
1094
|
+
let abandonTimer;
|
|
1095
|
+
const current = () => !settled && this.mounted && epoch === this.generation;
|
|
1096
|
+
const locked = [];
|
|
1097
|
+
const lockMethods = () => {
|
|
1098
|
+
for (const button of this.cardEl?.querySelectorAll('.method-btn, .passkey-create-entry, #passkey-btn, .locale-trigger, .locale-btn') ?? []) {
|
|
1099
|
+
locked.push({ button, disabled: button.disabled });
|
|
1100
|
+
button.disabled = true;
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1075
1103
|
const recovery = document.createElement('div');
|
|
1076
1104
|
recovery.className = 'oauth-recovery';
|
|
1077
1105
|
recovery.setAttribute('role', 'status');
|
|
1078
1106
|
const hint = document.createElement('p');
|
|
1079
|
-
|
|
1107
|
+
const actions = document.createElement('div');
|
|
1108
|
+
actions.className = 'oauth-recovery-actions';
|
|
1109
|
+
const reopen = document.createElement('button');
|
|
1110
|
+
reopen.type = 'button';
|
|
1111
|
+
reopen.className = 'oauth-retry';
|
|
1112
|
+
reopen.textContent = t('oauthReopen', locale);
|
|
1113
|
+
reopen.hidden = true;
|
|
1114
|
+
reopen.onclick = () => { this.select('oauth'); this.activeProvider = providerId; this.startOAuthPopup(providerId, btn); };
|
|
1080
1115
|
const fullPage = document.createElement('a');
|
|
1081
1116
|
fullPage.href = loginUrl;
|
|
1082
|
-
fullPage.textContent = t('oauthFullPage',
|
|
1083
|
-
fullPage.addEventListener('click',
|
|
1084
|
-
|
|
1085
|
-
|
|
1117
|
+
fullPage.textContent = t('oauthFullPage', locale);
|
|
1118
|
+
fullPage.addEventListener('click', event => {
|
|
1119
|
+
const reconcile = recovery.dataset.state === 'oauthUnconfirmed';
|
|
1120
|
+
if (exchanging && !settled) {
|
|
1121
|
+
event.preventDefault();
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
if (reconcile)
|
|
1125
|
+
event.preventDefault();
|
|
1126
|
+
this.options.onBeforeRedirect?.();
|
|
1127
|
+
this.oauthCleanup?.();
|
|
1128
|
+
if (reconcile)
|
|
1129
|
+
window.location.reload();
|
|
1130
|
+
});
|
|
1131
|
+
const cancel = document.createElement('button');
|
|
1132
|
+
cancel.type = 'button';
|
|
1133
|
+
cancel.className = 'oauth-cancel';
|
|
1134
|
+
cancel.textContent = t('cancel', locale);
|
|
1135
|
+
actions.append(reopen, fullPage, cancel);
|
|
1136
|
+
recovery.append(hint, actions);
|
|
1086
1137
|
btn.parentElement?.after(recovery);
|
|
1087
|
-
const
|
|
1088
|
-
recovery.hidden = false;
|
|
1089
|
-
this.emit('incomplete');
|
|
1138
|
+
const restoreButton = () => {
|
|
1090
1139
|
btn.innerHTML = originalHTML;
|
|
1091
1140
|
btn.disabled = false;
|
|
1092
1141
|
btn.classList.remove('oauth-btn-loading');
|
|
1093
1142
|
};
|
|
1094
|
-
|
|
1095
|
-
|
|
1143
|
+
const closePopup = () => { try {
|
|
1144
|
+
popup?.close();
|
|
1145
|
+
}
|
|
1146
|
+
catch { /* COOP can sever the window reference. */ } };
|
|
1147
|
+
const cleanup = () => {
|
|
1148
|
+
settled = true;
|
|
1149
|
+
clearInterval(pollTimer);
|
|
1150
|
+
clearInterval(sessionTimer);
|
|
1151
|
+
clearTimeout(abandonTimer);
|
|
1152
|
+
window.removeEventListener('message', onMessage);
|
|
1153
|
+
for (const item of locked.splice(0))
|
|
1154
|
+
item.button.disabled = item.disabled;
|
|
1155
|
+
restoreButton();
|
|
1156
|
+
};
|
|
1157
|
+
const showRecovery = (key) => {
|
|
1158
|
+
hint.textContent = t(key, locale, { provider });
|
|
1159
|
+
recovery.dataset.state = key;
|
|
1160
|
+
actions.hidden = false;
|
|
1161
|
+
reopen.hidden = false;
|
|
1162
|
+
cancel.hidden = true;
|
|
1163
|
+
restoreButton();
|
|
1164
|
+
};
|
|
1165
|
+
const fail = (key) => {
|
|
1166
|
+
if (!current())
|
|
1167
|
+
return;
|
|
1168
|
+
cleanup();
|
|
1169
|
+
closePopup();
|
|
1170
|
+
showRecovery(key);
|
|
1171
|
+
this.emit('incomplete');
|
|
1172
|
+
};
|
|
1096
1173
|
const succeed = () => {
|
|
1097
|
-
if (
|
|
1174
|
+
if (!current())
|
|
1098
1175
|
return;
|
|
1099
|
-
settled = true;
|
|
1100
1176
|
cleanup();
|
|
1101
|
-
|
|
1177
|
+
closePopup();
|
|
1178
|
+
recovery.remove();
|
|
1179
|
+
btn.textContent = t('success', locale);
|
|
1180
|
+
btn.disabled = true;
|
|
1102
1181
|
this.handleSuccess();
|
|
1103
1182
|
};
|
|
1183
|
+
cancel.onclick = () => { if (exchanging)
|
|
1184
|
+
return; fail('oauthCancelled'); btn.focus(); };
|
|
1104
1185
|
const onMessage = async (event) => {
|
|
1105
|
-
if (event.origin !== oauthOrigin || event.source !== popup)
|
|
1106
|
-
return;
|
|
1107
|
-
if (!event.data || event.data.type !== 'authorization_response')
|
|
1186
|
+
if (event.origin !== oauthOrigin || event.source !== popup || !popup)
|
|
1108
1187
|
return;
|
|
1109
|
-
if (
|
|
1188
|
+
if (!event.data || event.data.type !== 'authorization_response' || !current() || exchanging)
|
|
1110
1189
|
return;
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
// and the callback also window.close()s itself after postMessage.
|
|
1114
|
-
if (pollTimer !== undefined) {
|
|
1115
|
-
clearInterval(pollTimer);
|
|
1116
|
-
pollTimer = undefined;
|
|
1117
|
-
}
|
|
1118
|
-
try {
|
|
1119
|
-
popup.close();
|
|
1120
|
-
}
|
|
1121
|
-
catch {
|
|
1122
|
-
/* ignore */
|
|
1123
|
-
}
|
|
1190
|
+
clearInterval(pollTimer);
|
|
1191
|
+
clearInterval(sessionTimer);
|
|
1124
1192
|
if (event.data.error) {
|
|
1125
|
-
|
|
1126
|
-
cleanup();
|
|
1193
|
+
fail(event.data.error === 'access_denied' ? 'oauthDenied' : event.data.error === 'cancelled' ? 'oauthCancelled' : 'oauthFailed');
|
|
1127
1194
|
return;
|
|
1128
1195
|
}
|
|
1129
1196
|
const { code, state } = event.data.response ?? event.data;
|
|
1130
|
-
if (!code) {
|
|
1131
|
-
|
|
1132
|
-
cleanup();
|
|
1197
|
+
if (typeof code !== 'string' || !code || typeof state !== 'string' || !state) {
|
|
1198
|
+
fail('oauthFailed');
|
|
1133
1199
|
return;
|
|
1134
1200
|
}
|
|
1135
|
-
|
|
1201
|
+
exchanging = true;
|
|
1202
|
+
lockMethods();
|
|
1203
|
+
actions.hidden = true;
|
|
1204
|
+
closePopup();
|
|
1205
|
+
hint.textContent = t('verifying', locale);
|
|
1206
|
+
btn.textContent = t('verifying', locale);
|
|
1136
1207
|
btn.disabled = true;
|
|
1137
1208
|
try {
|
|
1138
1209
|
const oauthBody = { provider: providerId, code, state };
|
|
1139
1210
|
if (this.options.invitationId)
|
|
1140
1211
|
oauthBody.invitationId = this.options.invitationId;
|
|
1141
|
-
await this.http.post(`${servicePrefix}/api/oauth/login`, oauthBody);
|
|
1212
|
+
await this.http.post(`${servicePrefix}/api/oauth/login`, oauthBody, { signal: this.abort?.signal });
|
|
1142
1213
|
succeed();
|
|
1143
1214
|
}
|
|
1144
|
-
catch (
|
|
1145
|
-
if (
|
|
1215
|
+
catch (err) {
|
|
1216
|
+
if (!current())
|
|
1146
1217
|
return;
|
|
1147
|
-
const
|
|
1148
|
-
const
|
|
1149
|
-
if (
|
|
1150
|
-
settled = true;
|
|
1218
|
+
const code = err instanceof Error && 'code' in err ? String(err.code) : '';
|
|
1219
|
+
const message = err instanceof Error ? err.message : '';
|
|
1220
|
+
if (code === 'FORBIDDEN' || code === 'registration_closed' || message === 'registration_closed') {
|
|
1151
1221
|
cleanup();
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
else {
|
|
1155
|
-
restore();
|
|
1222
|
+
recovery.remove();
|
|
1223
|
+
this.showFullError(friendlyError(err, locale));
|
|
1156
1224
|
}
|
|
1225
|
+
else
|
|
1226
|
+
fail('oauthFailed');
|
|
1157
1227
|
}
|
|
1158
1228
|
};
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1229
|
+
this.oauthCleanup = () => { cleanup(); recovery.remove(); closePopup(); };
|
|
1230
|
+
window.addEventListener('message', onMessage);
|
|
1231
|
+
try {
|
|
1232
|
+
popup = window.open(loginUrl, 'oauth-login:popup', `left=${window.screenX + (window.innerWidth - 500) / 2},top=${window.screenY + (window.innerHeight - 620) / 2},width=500,height=620,resizable,scrollbars=yes,status=1,popup`);
|
|
1233
|
+
}
|
|
1234
|
+
catch { /* Treat a browser refusal like a blocked popup. */ }
|
|
1235
|
+
if (!popup) {
|
|
1236
|
+
fail('oauthBlocked');
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
hint.textContent = t('oauthWaiting', locale, { provider });
|
|
1240
|
+
btn.textContent = t('oauthWaiting', locale, { provider });
|
|
1241
|
+
btn.disabled = true;
|
|
1242
|
+
btn.classList.add('oauth-btn-loading');
|
|
1243
|
+
// COOP can report closed before the callback arrives. Keep the listener,
|
|
1244
|
+
// but show recovery immediately and never overlap server session probes.
|
|
1245
|
+
const checkSession = async () => {
|
|
1246
|
+
if (!current() || exchanging || probing)
|
|
1247
|
+
return;
|
|
1248
|
+
probing = true;
|
|
1249
|
+
try {
|
|
1250
|
+
const session = await this.http.get(`${servicePrefix}/api/did/session`);
|
|
1251
|
+
if (current() && !exchanging && session?.authenticated === true)
|
|
1252
|
+
succeed();
|
|
1253
|
+
}
|
|
1254
|
+
catch { /* A later probe or callback may still complete this attempt. */ }
|
|
1255
|
+
finally {
|
|
1256
|
+
probing = false;
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
pollTimer = setInterval(() => {
|
|
1163
1260
|
let closed = false;
|
|
1164
1261
|
try {
|
|
1165
|
-
closed = popup
|
|
1262
|
+
closed = !!popup?.closed;
|
|
1166
1263
|
}
|
|
1167
1264
|
catch {
|
|
1168
1265
|
return;
|
|
1169
1266
|
}
|
|
1170
|
-
if (!closed)
|
|
1267
|
+
if (!closed || !current())
|
|
1171
1268
|
return;
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
void this.http
|
|
1179
|
-
.get(`${servicePrefix}/api/did/session`)
|
|
1180
|
-
.then((session) => {
|
|
1181
|
-
if (session?.authenticated)
|
|
1182
|
-
succeed();
|
|
1183
|
-
})
|
|
1184
|
-
.catch(() => {
|
|
1185
|
-
/* keep waiting for postMessage or the next session probe */
|
|
1186
|
-
});
|
|
1187
|
-
};
|
|
1188
|
-
checkSession();
|
|
1189
|
-
sessionPollTimer = setInterval(checkSession, 500);
|
|
1269
|
+
clearInterval(pollTimer);
|
|
1270
|
+
showRecovery('oauthIncomplete');
|
|
1271
|
+
// Remain explicitly cancellable while a COOP callback may still arrive.
|
|
1272
|
+
cancel.hidden = false;
|
|
1273
|
+
void checkSession();
|
|
1274
|
+
sessionTimer = setInterval(() => { void checkSession(); }, 2000);
|
|
1190
1275
|
}, 500);
|
|
1191
|
-
|
|
1192
|
-
if (
|
|
1276
|
+
abandonTimer = setTimeout(() => {
|
|
1277
|
+
if (!current())
|
|
1193
1278
|
return;
|
|
1279
|
+
if (!exchanging) {
|
|
1280
|
+
fail('oauthTimedOut');
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
// A request may have reached the server even if its response never arrived.
|
|
1284
|
+
// Do not promise cancellation or start a second exchange in that ambiguity.
|
|
1285
|
+
this.abort?.abort();
|
|
1194
1286
|
cleanup();
|
|
1195
|
-
|
|
1287
|
+
closePopup();
|
|
1288
|
+
lockMethods();
|
|
1289
|
+
hint.textContent = t('oauthUnconfirmed', locale);
|
|
1290
|
+
recovery.dataset.state = 'oauthUnconfirmed';
|
|
1291
|
+
actions.hidden = false;
|
|
1292
|
+
reopen.hidden = true;
|
|
1293
|
+
cancel.hidden = true;
|
|
1294
|
+
fullPage.href = window.location.href;
|
|
1295
|
+
fullPage.textContent = t('oauthCheckSession', locale);
|
|
1296
|
+
this.emit('incomplete');
|
|
1196
1297
|
}, 5 * 60 * 1000);
|
|
1197
|
-
const cleanup = () => {
|
|
1198
|
-
settled = true;
|
|
1199
|
-
btn.innerHTML = originalHTML;
|
|
1200
|
-
btn.disabled = false;
|
|
1201
|
-
btn.classList.remove('oauth-btn-loading');
|
|
1202
|
-
if (pollTimer !== undefined) {
|
|
1203
|
-
clearInterval(pollTimer);
|
|
1204
|
-
pollTimer = undefined;
|
|
1205
|
-
}
|
|
1206
|
-
if (sessionPollTimer !== undefined) {
|
|
1207
|
-
clearInterval(sessionPollTimer);
|
|
1208
|
-
sessionPollTimer = undefined;
|
|
1209
|
-
}
|
|
1210
|
-
clearTimeout(abandonTimer);
|
|
1211
|
-
window.removeEventListener('message', onMessage);
|
|
1212
|
-
};
|
|
1213
|
-
this.oauthCleanup = () => { cleanup(); recovery.remove(); try {
|
|
1214
|
-
popup.close();
|
|
1215
|
-
}
|
|
1216
|
-
catch { /* COOP */ } };
|
|
1217
|
-
window.addEventListener('message', onMessage);
|
|
1218
1298
|
}
|
|
1219
1299
|
// (removed dead code: renderEmailButton — email now renders through renderSecondaryGrid)
|
|
1220
1300
|
/**
|
|
@@ -1240,6 +1320,7 @@ export class LoginPage {
|
|
|
1240
1320
|
btn.dataset.provider = provider.id;
|
|
1241
1321
|
const icon = getOAuthIcon(provider.id);
|
|
1242
1322
|
btn.innerHTML = `${icon} ${escapeHtml(t('continueWith', locale, { provider: provider.name }))}`;
|
|
1323
|
+
this.markLastUsed(btn, `oauth:${provider.id}`, locale);
|
|
1243
1324
|
grid.appendChild(btn);
|
|
1244
1325
|
btn.addEventListener('click', () => {
|
|
1245
1326
|
this.select('oauth');
|
|
@@ -1254,6 +1335,7 @@ export class LoginPage {
|
|
|
1254
1335
|
btn.className = 'method-btn';
|
|
1255
1336
|
btn.id = 'email-btn';
|
|
1256
1337
|
btn.innerHTML = `${ICON_EMAIL} ${escapeHtml(t('emailButton', locale))}`;
|
|
1338
|
+
this.markLastUsed(btn, 'email', locale);
|
|
1257
1339
|
grid.appendChild(btn);
|
|
1258
1340
|
btn.addEventListener('click', () => {
|
|
1259
1341
|
this.select('email');
|
|
@@ -1268,6 +1350,7 @@ export class LoginPage {
|
|
|
1268
1350
|
// Use short label when sharing grid with other methods
|
|
1269
1351
|
const walletLabel = grid.children.length > 0 ? t('didWalletButtonShort', locale) : t('didWalletButton', locale);
|
|
1270
1352
|
btn.innerHTML = `${ICON_DID_WALLET} ${escapeHtml(walletLabel)}`;
|
|
1353
|
+
this.markLastUsed(btn, 'did-connect', locale);
|
|
1271
1354
|
grid.appendChild(btn);
|
|
1272
1355
|
btn.addEventListener('click', () => {
|
|
1273
1356
|
this.select('did-connect');
|
|
@@ -1308,8 +1391,9 @@ export class LoginPage {
|
|
|
1308
1391
|
}
|
|
1309
1392
|
});
|
|
1310
1393
|
this.tokenSession.on('timeout', () => {
|
|
1311
|
-
statusEl.textContent =
|
|
1312
|
-
//
|
|
1394
|
+
statusEl.textContent = '';
|
|
1395
|
+
// Cover the expired code itself. The notice lives on that plate, so the
|
|
1396
|
+
// status line under the wallet button does not repeat it.
|
|
1313
1397
|
const overlay = document.createElement('div');
|
|
1314
1398
|
overlay.className = 'qr-timeout-overlay';
|
|
1315
1399
|
overlay.innerHTML = `<span class="timeout-text">${escapeHtml(t('timeout', locale))}</span>`;
|
|
@@ -1324,7 +1408,8 @@ export class LoginPage {
|
|
|
1324
1408
|
this.startDIDConnectInView(statusEl, qrContainer, walletBtn, locale);
|
|
1325
1409
|
});
|
|
1326
1410
|
overlay.appendChild(refreshBtn);
|
|
1327
|
-
qrContainer.
|
|
1411
|
+
const plate = qrContainer.querySelector('.qr-plate');
|
|
1412
|
+
(plate ?? qrContainer).appendChild(overlay);
|
|
1328
1413
|
});
|
|
1329
1414
|
this.tokenSession.on('error', (err) => {
|
|
1330
1415
|
this.showConnectResult(qrContainer, walletBtn, statusEl, friendlyError(err, locale), true);
|