feat(identity): align auth flow with reference theme
This commit is contained in:
1 parent
5a17b399a0
commit
59901b9e10
34 files changed
+753
-3871
No files matched your search
@@ -1,364 +1,113 @@
|
||||
(function (window, document) {
|
||||
'use strict';
|
||||
|
||||
function parseConfig(root) {
|
||||
function configFor(root) {
|
||||
try { return JSON.parse(root.getAttribute('data-identity-config') || '{}'); } catch (_) { return {}; }
|
||||
}
|
||||
|
||||
function uuid() {
|
||||
if (window.crypto && crypto.randomUUID) return crypto.randomUUID();
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
var r = Math.random() * 16 | 0;
|
||||
return (c === 'x' ? r : (r & 3 | 8)).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
function request(path, config, options) {
|
||||
options = Object.assign({}, options || {});
|
||||
options.credentials = 'same-origin';
|
||||
options.headers = Object.assign({ 'X-WP-Nonce': config.nonce || '' }, options.headers || {});
|
||||
return window.AksrtRequest.request(path, options).then(function (response) {
|
||||
return response.data !== undefined ? response.data : response.json;
|
||||
});
|
||||
function parseResponse(response) {
|
||||
var payload = response && (response.data !== undefined ? response.data : response.json);
|
||||
return payload && payload.data !== undefined ? payload.data : payload;
|
||||
}
|
||||
|
||||
function errorMessage(error, fallback) {
|
||||
if (error && error.json) {
|
||||
return error.json.message || (error.json.data && error.json.data.message) || fallback;
|
||||
}
|
||||
if (error && error.json) return error.json.message || fallback;
|
||||
return error && error.message ? error.message : fallback;
|
||||
}
|
||||
|
||||
function loadScript(src) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var existing = document.querySelector('script[src="' + src + '"]');
|
||||
if (existing) {
|
||||
if (existing.dataset.loaded === '1') return resolve();
|
||||
existing.addEventListener('load', resolve, { once: true });
|
||||
existing.addEventListener('error', reject, { once: true });
|
||||
return;
|
||||
}
|
||||
var script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.async = true;
|
||||
script.onload = function () { script.dataset.loaded = '1'; resolve(); };
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
function aliyunMetaInfo(config) {
|
||||
if (config.provider !== 'aliyun') return Promise.resolve('');
|
||||
return loadScript('https://o.alicdn.com/yd-cloudauth/cloudauth-cdn/jsvm_all.js').then(function () {
|
||||
if (typeof window.getMetaInfo !== 'function') throw new Error('阿里云 MetaInfo 组件未加载成功');
|
||||
var meta = window.getMetaInfo();
|
||||
return typeof meta === 'string' ? meta : JSON.stringify(meta || {});
|
||||
});
|
||||
}
|
||||
|
||||
function init(root) {
|
||||
if (!root || root.dataset.identityBound === '1') return;
|
||||
root.dataset.identityBound = '1';
|
||||
|
||||
var config = parseConfig(root);
|
||||
var config = configFor(root);
|
||||
var template = root.querySelector('[data-auth-modal-template]');
|
||||
var runtime = window.AksrtModalRuntime;
|
||||
if (!template) return;
|
||||
|
||||
var activeShell = null;
|
||||
var currentUuid = config.currentUuid || '';
|
||||
var currentStage = config.currentStage || '';
|
||||
var pollTimer = null;
|
||||
var pollBackoff = 0;
|
||||
var currentController = null;
|
||||
var refreshController = null;
|
||||
var refreshTimeout = null;
|
||||
var refreshInFlight = false;
|
||||
var openSequence = 0;
|
||||
var pendingCreateKey = '';
|
||||
var pendingCreateSignature = '';
|
||||
var pendingRefreshKey = '';
|
||||
var pendingManualKey = '';
|
||||
var pendingManualSignature = '';
|
||||
|
||||
function isOpen(shell) {
|
||||
return !!shell && shell.classList.contains('is-open') && document.contains(shell);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollTimer) window.clearTimeout(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
|
||||
function abortCurrent() {
|
||||
if (currentController) {
|
||||
try { currentController.abort(); } catch (_) {}
|
||||
}
|
||||
currentController = null;
|
||||
}
|
||||
|
||||
function abortRefresh() {
|
||||
if (refreshTimeout) window.clearTimeout(refreshTimeout);
|
||||
refreshTimeout = null;
|
||||
if (refreshController) {
|
||||
try { refreshController.abort(); } catch (_) {}
|
||||
}
|
||||
refreshController = null;
|
||||
refreshInFlight = false;
|
||||
}
|
||||
|
||||
function setMessage(shell, message) {
|
||||
var node = shell && shell.querySelector('[data-auth-status-message]');
|
||||
if (node) node.textContent = message || '';
|
||||
}
|
||||
|
||||
function updateSummary(data) {
|
||||
if (!data || data.status !== 'verified') return;
|
||||
var trigger = root.querySelector('[data-auth-modal-open]');
|
||||
if (trigger) {
|
||||
trigger.disabled = true;
|
||||
trigger.textContent = '已认证';
|
||||
}
|
||||
runtime.invalidate(['identity']);
|
||||
document.dispatchEvent(new CustomEvent('aksrt:identity:changed', { detail: { status: data.status } }));
|
||||
}
|
||||
|
||||
function schedulePoll(delay) {
|
||||
stopPolling();
|
||||
if (!currentUuid || !isOpen(activeShell) || document.visibilityState !== 'visible') return;
|
||||
var base = Math.max(5, Number(delay || config.pollInterval || 5));
|
||||
var seconds = Math.min(60, base * Math.pow(2, Math.min(4, pollBackoff)));
|
||||
pollTimer = window.setTimeout(function () {
|
||||
pollTimer = null;
|
||||
refresh(false);
|
||||
}, seconds * 1000);
|
||||
}
|
||||
|
||||
function render(data, shell) {
|
||||
if (!shell || shell !== activeShell || !isOpen(shell)) return;
|
||||
data = data || {};
|
||||
var previousStage = currentStage;
|
||||
currentUuid = data.uuid || currentUuid;
|
||||
currentStage = data.stage || currentStage;
|
||||
if (data.stage && data.stage !== previousStage) runtime.invalidate(['identity']);
|
||||
var lock = ['created', 'provider_pending', 'provider_processing', 'manual_pending'].indexOf(currentStage) !== -1;
|
||||
var retryable = data.next_action === 'manual_or_retry';
|
||||
var statusBox = shell.querySelector('[data-auth-status]');
|
||||
var createForm = shell.querySelector('[data-auth-create]');
|
||||
var stageNode = shell.querySelector('[data-auth-stage]');
|
||||
var messageNode = shell.querySelector('[data-auth-status-message]');
|
||||
var continueLink = shell.querySelector('[data-auth-continue]');
|
||||
var manualButton = shell.querySelector('[data-auth-manual-open]');
|
||||
|
||||
if (statusBox) statusBox.hidden = !data.uuid && !retryable;
|
||||
if (createForm) createForm.hidden = lock || data.status === 'verified';
|
||||
if (stageNode) stageNode.textContent = data.stage || data.status || '准备认证';
|
||||
if (messageNode) messageNode.textContent = data.message || '认证结果以站点服务端主动查询为准。';
|
||||
if (continueLink) {
|
||||
var canContinue = data.next_action === 'poll' && data.redirect_url;
|
||||
continueLink.hidden = !canContinue;
|
||||
if (canContinue) continueLink.href = data.redirect_url;
|
||||
}
|
||||
if (manualButton) manualButton.hidden = !retryable;
|
||||
|
||||
if (data.status === 'verified' || data.next_action !== 'poll') {
|
||||
pollBackoff = 0;
|
||||
stopPolling();
|
||||
} else if (isOpen(shell) && document.visibilityState === 'visible') {
|
||||
schedulePoll(data.poll_interval || config.pollInterval);
|
||||
}
|
||||
updateSummary(data);
|
||||
}
|
||||
|
||||
function refresh(explicit) {
|
||||
if (!currentUuid || !isOpen(activeShell) || document.visibilityState !== 'visible' || refreshInFlight) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
stopPolling();
|
||||
abortRefresh();
|
||||
refreshInFlight = true;
|
||||
refreshController = window.AbortController ? new window.AbortController() : null;
|
||||
var timedOut = false;
|
||||
var shell = activeShell;
|
||||
var timeoutMs = Math.max(5000, Number(config.refreshTimeout || 15000));
|
||||
if (!pendingRefreshKey) pendingRefreshKey = uuid();
|
||||
refreshTimeout = window.setTimeout(function () {
|
||||
timedOut = true;
|
||||
if (refreshController) {
|
||||
try { refreshController.abort(); } catch (_) {}
|
||||
}
|
||||
}, timeoutMs);
|
||||
|
||||
return request(config.restBase + '/' + encodeURIComponent(currentUuid) + '/refresh', config, {
|
||||
method: 'POST',
|
||||
headers: { 'Idempotency-Key': pendingRefreshKey },
|
||||
signal: refreshController ? refreshController.signal : undefined,
|
||||
}).then(function (data) {
|
||||
pendingRefreshKey = '';
|
||||
if (shell !== activeShell || !isOpen(shell)) return data;
|
||||
pollBackoff = explicit ? 0 : Math.min(4, pollBackoff + 1);
|
||||
render(data, shell);
|
||||
return data;
|
||||
}).catch(function (error) {
|
||||
var aborted = error && (error.name === 'AbortError' || error.code === 'ABORT_ERR');
|
||||
if (shell !== activeShell || !isOpen(shell) || (aborted && !timedOut)) return null;
|
||||
setMessage(shell, timedOut ? '云端刷新超时,请稍后重试。' : errorMessage(error, '刷新失败,请稍后重试。'));
|
||||
pollBackoff = Math.min(4, pollBackoff + 1);
|
||||
schedulePoll(config.pollInterval || 5);
|
||||
return null;
|
||||
}).finally(function () {
|
||||
if (refreshTimeout) window.clearTimeout(refreshTimeout);
|
||||
refreshTimeout = null;
|
||||
refreshController = null;
|
||||
refreshInFlight = false;
|
||||
});
|
||||
}
|
||||
|
||||
function loadCurrent(shell, sequence) {
|
||||
abortCurrent();
|
||||
currentController = window.AbortController ? new window.AbortController() : null;
|
||||
var controller = currentController;
|
||||
return request(config.restBase + '/current', config, {
|
||||
method: 'GET',
|
||||
signal: controller ? controller.signal : undefined,
|
||||
}).then(function (snapshot) {
|
||||
if (sequence !== openSequence || shell !== activeShell || !isOpen(shell)) return;
|
||||
var data = snapshot && (snapshot.current || snapshot.active || snapshot.latest);
|
||||
if (!data) {
|
||||
currentUuid = '';
|
||||
currentStage = '';
|
||||
var statusBox = shell.querySelector('[data-auth-status]');
|
||||
var createForm = shell.querySelector('[data-auth-create]');
|
||||
if (statusBox) statusBox.hidden = true;
|
||||
if (createForm) createForm.hidden = false;
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
render(data, shell);
|
||||
}).catch(function (error) {
|
||||
var aborted = error && (error.name === 'AbortError' || error.code === 'ABORT_ERR');
|
||||
if (aborted || sequence !== openSequence || shell !== activeShell || !isOpen(shell)) return;
|
||||
setMessage(shell, errorMessage(error, '状态加载失败,请稍后重试。'));
|
||||
}).finally(function () {
|
||||
if (currentController === controller) currentController = null;
|
||||
});
|
||||
}
|
||||
|
||||
function bindForm(shell) {
|
||||
var createForm = shell.querySelector('[data-auth-create]');
|
||||
var manualForm = shell.querySelector('[data-auth-manual]');
|
||||
var statusBox = shell.querySelector('[data-auth-status]');
|
||||
var refreshButton = shell.querySelector('[data-auth-refresh]');
|
||||
var manualButton = shell.querySelector('[data-auth-manual-open]');
|
||||
var typeSelect = createForm && createForm.querySelector('[name="type"]');
|
||||
var enterprise = createForm && createForm.querySelector('[data-enterprise-fields]');
|
||||
var personLabel = createForm && createForm.querySelector('[data-auth-person-label]');
|
||||
var idLabel = createForm && createForm.querySelector('[data-auth-id-label]');
|
||||
|
||||
function syncEnterprise() {
|
||||
var enabled = typeSelect && typeSelect.value === 'enterprise';
|
||||
if (enterprise) enterprise.hidden = !enabled;
|
||||
if (enterprise) enterprise.querySelectorAll('input').forEach(function (input) { input.required = enabled; });
|
||||
if (personLabel) personLabel.textContent = enabled ? '法定代表人姓名' : '真实姓名';
|
||||
if (idLabel) idLabel.textContent = enabled ? '法定代表人身份证号' : '身份证号码';
|
||||
}
|
||||
|
||||
if (typeSelect) {
|
||||
typeSelect.addEventListener('change', syncEnterprise);
|
||||
syncEnterprise();
|
||||
}
|
||||
if (refreshButton) refreshButton.addEventListener('click', function () { refresh(true); });
|
||||
if (manualButton) manualButton.addEventListener('click', function () { if (manualForm) manualForm.hidden = false; });
|
||||
|
||||
if (createForm) createForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
var button = createForm.querySelector('[type="submit"]');
|
||||
var message = createForm.querySelector('[data-auth-message]');
|
||||
var form = new FormData(createForm);
|
||||
var payload = {};
|
||||
form.forEach(function (value, key) { payload[key] = value; });
|
||||
payload.consent = payload.consent === '1';
|
||||
var signature = JSON.stringify(payload);
|
||||
if (!pendingCreateKey || pendingCreateSignature !== signature) {
|
||||
pendingCreateKey = uuid();
|
||||
pendingCreateSignature = signature;
|
||||
}
|
||||
if (button) button.disabled = true;
|
||||
if (message) message.textContent = '';
|
||||
aliyunMetaInfo(config).then(function (metaInfo) {
|
||||
if (metaInfo) payload.meta_info = metaInfo;
|
||||
return request(config.restBase, config, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': pendingCreateKey },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}).then(function (data) {
|
||||
pendingCreateKey = '';
|
||||
pendingCreateSignature = '';
|
||||
render(data, shell);
|
||||
}).catch(function (error) {
|
||||
if (message) message.textContent = errorMessage(error, '创建认证会话失败,请重试。');
|
||||
}).finally(function () {
|
||||
if (button) button.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
if (manualForm) manualForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
if (!currentUuid) return;
|
||||
var files = manualForm.querySelector('[name="evidence[]"]');
|
||||
if (files && files.files.length > 3) {
|
||||
setMessage(shell, '证明材料最多上传 3 张');
|
||||
return;
|
||||
}
|
||||
var button = manualForm.querySelector('[type="submit"]');
|
||||
var body = new FormData();
|
||||
Array.prototype.forEach.call((files && files.files) || [], function (file) { body.append('evidence[]', file); });
|
||||
var signature = Array.prototype.map.call((files && files.files) || [], function (file) {
|
||||
return [file.name, file.size, file.lastModified].join(':');
|
||||
}).join('|');
|
||||
if (!pendingManualKey || pendingManualSignature !== signature) {
|
||||
pendingManualKey = uuid();
|
||||
pendingManualSignature = signature;
|
||||
}
|
||||
if (button) button.disabled = true;
|
||||
request(config.restBase + '/' + encodeURIComponent(currentUuid) + '/manual', config, {
|
||||
method: 'POST',
|
||||
headers: { 'Idempotency-Key': pendingManualKey },
|
||||
body: body,
|
||||
}).then(function (data) {
|
||||
pendingManualKey = '';
|
||||
pendingManualSignature = '';
|
||||
manualForm.hidden = true;
|
||||
render(data, shell);
|
||||
}).catch(function (error) {
|
||||
setMessage(shell, errorMessage(error, '人工审核提交失败,请重试。'));
|
||||
}).finally(function () {
|
||||
if (button) button.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
if (statusBox && statusBox.hidden && createForm) createForm.hidden = false;
|
||||
}
|
||||
if (!template || !runtime || !window.AksrtRequest) return;
|
||||
|
||||
function open() {
|
||||
stopPolling();
|
||||
abortCurrent();
|
||||
abortRefresh();
|
||||
openSequence += 1;
|
||||
var sequence = openSequence;
|
||||
var shell = runtime.open({
|
||||
runtime.open({
|
||||
html: template.innerHTML,
|
||||
kind: 'identity',
|
||||
className: 'aksrt-identity-modal-shell',
|
||||
mobileBottom: true,
|
||||
onContent: function (_, session) {
|
||||
activeShell = session.shell;
|
||||
bindForm(session.shell);
|
||||
var shell = session && session.shell;
|
||||
var form = shell && shell.querySelector('[data-auth-create]');
|
||||
if (!form) return;
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
var input = form.querySelector('[name="evidence[]"]');
|
||||
var message = form.querySelector('[data-auth-message]');
|
||||
var button = form.querySelector('[type="submit"]');
|
||||
var files = input ? Array.prototype.slice.call(input.files || []) : [];
|
||||
if (files.length > 3) {
|
||||
if (message) message.textContent = '证明材料最多上传 3 张';
|
||||
return;
|
||||
}
|
||||
if (files.some(function (file) { return file.size > 2 * 1024 * 1024; })) {
|
||||
if (message) message.textContent = '单张证明材料不能超过 2MB';
|
||||
return;
|
||||
}
|
||||
|
||||
if (button) button.disabled = true;
|
||||
if (message) message.textContent = '';
|
||||
window.AksrtRequest.request(config.restBase, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'X-WP-Nonce': config.nonce || '' },
|
||||
body: new FormData(form),
|
||||
}).then(function (response) {
|
||||
var result = parseResponse(response);
|
||||
if (!result || result.success === false) throw new Error((result && result.message) || '申请提交失败,请稍后重试');
|
||||
if (message) message.textContent = '申请已提交,请耐心等待管理员审核。';
|
||||
var summary = root.querySelector('[data-auth-summary]');
|
||||
if (summary) {
|
||||
summary.replaceChildren();
|
||||
var pending = document.createElement('div');
|
||||
pending.className = 'c-red mb10';
|
||||
pending.textContent = '您的申请正在审核中,请耐心等待';
|
||||
var name = document.createElement('div');
|
||||
name.textContent = result.name || '';
|
||||
summary.appendChild(pending);
|
||||
summary.appendChild(name);
|
||||
if (result.description) {
|
||||
var description = document.createElement('div');
|
||||
description.className = 'muted-2-color mt10';
|
||||
description.textContent = result.description;
|
||||
summary.appendChild(description);
|
||||
}
|
||||
if (result.message) {
|
||||
var applicationMessage = document.createElement('div');
|
||||
applicationMessage.className = 'muted-2-color mt10';
|
||||
applicationMessage.textContent = '申请说明:' + result.message;
|
||||
summary.appendChild(applicationMessage);
|
||||
}
|
||||
(Array.isArray(result.evidence) ? result.evidence : []).forEach(function (item) {
|
||||
if (!item || !item.url || !item.thumbnail_url) return;
|
||||
var link = document.createElement('a');
|
||||
link.href = item.url;
|
||||
link.target = '_blank';
|
||||
link.rel = 'noopener noreferrer';
|
||||
var image = document.createElement('img');
|
||||
image.src = item.thumbnail_url;
|
||||
image.alt = '身份认证证明材料';
|
||||
image.width = 64;
|
||||
image.height = 64;
|
||||
image.style.objectFit = 'cover';
|
||||
image.style.borderRadius = '4px';
|
||||
link.appendChild(image);
|
||||
summary.appendChild(link);
|
||||
});
|
||||
}
|
||||
if (runtime.getActive && runtime.getActive() === shell) runtime.close('submit');
|
||||
}).catch(function (error) {
|
||||
if (message) message.textContent = errorMessage(error, '申请提交失败,请稍后重试');
|
||||
}).finally(function () {
|
||||
if (button) button.disabled = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
activeShell = shell;
|
||||
loadCurrent(shell, sequence);
|
||||
}
|
||||
|
||||
root.querySelectorAll('[data-auth-modal-open]').forEach(function (button) {
|
||||
@@ -367,27 +116,6 @@
|
||||
open();
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (!isOpen(activeShell)) return;
|
||||
if (document.visibilityState !== 'visible') {
|
||||
stopPolling();
|
||||
abortCurrent();
|
||||
abortRefresh();
|
||||
return;
|
||||
}
|
||||
if (currentUuid && ['provider_pending', 'provider_processing'].indexOf(currentStage) !== -1) {
|
||||
schedulePoll(config.pollInterval || 5);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('aksrt:modal:close', function (event) {
|
||||
if (!activeShell || !event.detail || event.detail.shell !== activeShell) return;
|
||||
stopPolling();
|
||||
abortCurrent();
|
||||
abortRefresh();
|
||||
activeShell = null;
|
||||
});
|
||||
}
|
||||
|
||||
window.AKSRTUserCenterAuth = { init: init };
|
||||
|
||||
Reference in new issue
Block a user