395 lines
16 KiB
JavaScript
395 lines
16 KiB
JavaScript
(function (window, document) {
|
|
'use strict';
|
|
|
|
function parseConfig(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 errorMessage(error, fallback) {
|
|
if (error && error.json) {
|
|
return error.json.message || (error.json.data && error.json.data.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 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;
|
|
}
|
|
|
|
function open() {
|
|
stopPolling();
|
|
abortCurrent();
|
|
abortRefresh();
|
|
openSequence += 1;
|
|
var sequence = openSequence;
|
|
var shell = runtime.open({
|
|
html: template.innerHTML,
|
|
kind: 'identity',
|
|
className: 'aksrt-identity-modal-shell',
|
|
mobileBottom: true,
|
|
onContent: function (_, session) {
|
|
activeShell = session.shell;
|
|
bindForm(session.shell);
|
|
},
|
|
});
|
|
activeShell = shell;
|
|
loadCurrent(shell, sequence);
|
|
}
|
|
|
|
root.querySelectorAll('[data-auth-modal-open]').forEach(function (button) {
|
|
button.addEventListener('click', function (event) {
|
|
event.preventDefault();
|
|
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 };
|
|
})(window, document);
|