mirror of
https://github.com/Silentely/eSIM-Tools.git
synced 2026-09-03 06:24:20 +08:00
🐛 fix: 修复 giffgaff session 恢复后 LPA 和二维码不显示的问题(Issue #75)
- `showESimResult` 中移除 `!state.lpaString` 的提前返回,即使 lpaString 为空也显示结果容器,确保用户看到明确的错误提示和可能的排查信息 - 在 session 恢复流程(`giffgaff-app.js`)中显式调用 `uiController.showESimResult()`,使刷新页面后能正确显示二维码和 LPA 字符串,而不再跳过关键渲染步骤 - 为 `showESimResult` 添加完整的 try-catch 包裹及防御性 DOM 元素校验,在二维码生成失败时降级显示错误提示,避免页面静默失败 - 新增 i18n 键 `giffgaff.app.error.lpaStringMissing`(中英文),用于替代硬编码的错误文案,保持国际化一致性 - 新增 `tests/giffgaff/session-restore-lpa.test.js`,覆盖正常 LPA 展示、空 lpaString 错误降级、二维码生成、状态不一致降级以及延迟显示等场景
This commit is contained in:
34
CLAUDE.md
34
CLAUDE.md
@@ -115,10 +115,44 @@
|
||||
|
||||
---
|
||||
|
||||
## 🧠 项目记忆管理
|
||||
|
||||
### 项目容器标签
|
||||
|
||||
**containerTag**: `esim-tools`
|
||||
|
||||
### 关键记忆点
|
||||
|
||||
#### 技术栈决策
|
||||
- **无框架设计**:坚持原生 JavaScript,避免 React/Vue 依赖,保持最小打包体积
|
||||
- **Serverless 架构**:Netlify Functions + Edge Functions,BFF 模式代理
|
||||
- **原生 ES6 模块**:业务页面不经 Webpack 打包,浏览器原生加载
|
||||
|
||||
#### 踩坑记录
|
||||
- **Giffgaff OAuth REDIRECT_URI**:环境变量只影响服务端 token exchange,前端授权 URL 由 `api-config.js` 独立控制
|
||||
- **Netlify Free Plan 限制**:Functions 每月 125,000 次调用配额,避免前端轮询
|
||||
- **Safari 隐私模式**:IndexedDB 会抛出 QuotaExceededError,需降级到 sessionStorage
|
||||
|
||||
#### 编码偏好
|
||||
- 缩进:2 空格
|
||||
- 引号:单引号优先
|
||||
- 分号:必须使用
|
||||
- 命名:camelCase(变量/函数),PascalCase(类),kebab-case(文件)
|
||||
|
||||
### 自动记忆触发
|
||||
|
||||
- 修改 OAuth/认证流程 → 检索 "OAuth PKCE 认证 零信任"
|
||||
- 添加 Netlify Functions → 检索 "Functions 中间件 withAuth"
|
||||
- 性能优化 → 检索 "性能 瓶颈 IndexedDB"
|
||||
- 部署问题 → 检索 "Netlify 部署 环境变量"
|
||||
|
||||
---
|
||||
|
||||
## 变更记录
|
||||
|
||||
| 时间 | 变更内容 |
|
||||
|------|----------|
|
||||
| 2026-06-12 | 集成 Not-ace-memory 记忆管理系统 |
|
||||
| 2026-06-02 22:54:25 | 增量扫描更新:为 src/giffgaff 和 src/simyo 新建 CLAUDE.md,修复模块结构图 click 链接,校正脚本数量 |
|
||||
| 2026-05-25 | 清理 Legacy 残留措辞:删除 verify-legacy-frozen 工具链、修复 server.js 死路由 simyo-static、统一架构表述为"原生 ES6 模块" |
|
||||
| 2026-05-18 | 文档清理与路径重构:移除未使用的新模块化版本,修复文档错链,精简重复文档 |
|
||||
|
||||
@@ -1242,8 +1242,12 @@ class GiffgaffApp {
|
||||
if (state.memberId && (state.esimActivationCode || state.esimSSN)) {
|
||||
if (state.lpaString) {
|
||||
targetStep = 5;
|
||||
// LPA已获取,提示清理
|
||||
// LPA已获取,显示二维码和 LPA 信息(修复 Issue #75)
|
||||
setTimeout(() => {
|
||||
// 显示二维码和 LPA 字符串
|
||||
console.log('[Giffgaff] 从 session 恢复 LPA,显示二维码和 LPA 信息');
|
||||
uiController.showESimResult();
|
||||
|
||||
uiController.showStatus(
|
||||
uiController.elements.tokenStatus,
|
||||
t('giffgaff.app.status.lpaFetchedOnce'),
|
||||
|
||||
@@ -535,12 +535,55 @@ export class UIController {
|
||||
*/
|
||||
showESimResult() {
|
||||
const state = stateManager.getState();
|
||||
if (!state.lpaString) return;
|
||||
|
||||
this.elements.resultContainer.classList.add('active');
|
||||
this.generateQRCode(state.lpaString);
|
||||
try {
|
||||
// 防御性检查:即使 lpaString 为空也显示容器,给用户明确提示(修复 Issue #75)
|
||||
this.elements.resultContainer.classList.add('active');
|
||||
|
||||
this.elements.esimInfo.innerHTML = `
|
||||
if (!state.lpaString) {
|
||||
// LPA 字符串缺失,显示错误提示和调试信息
|
||||
console.error('[Giffgaff] showESimResult called but lpaString is empty');
|
||||
this.elements.qrcode.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
${t('giffgaff.app.error.lpaStringMissing') || '获取 LPA 字符串失败'}
|
||||
</div>
|
||||
`;
|
||||
this.elements.esimInfo.innerHTML = `
|
||||
<div class="alert alert-warning">
|
||||
<p><strong>${tl('可能原因')}:</strong></p>
|
||||
<ul class="mb-0">
|
||||
<li>后端轮询超时或失败</li>
|
||||
<li>GraphQL 响应异常</li>
|
||||
<li>网络连接中断</li>
|
||||
<li>eSIM 状态异常</li>
|
||||
</ul>
|
||||
<hr class="my-2">
|
||||
<p class="mb-0">
|
||||
<small>
|
||||
${tl('请打开浏览器控制台(F12)查看详细日志')}
|
||||
<br>
|
||||
<a href="https://github.com/Silentely/eSIM-Tools/issues" target="_blank" rel="noopener noreferrer">
|
||||
${tl('提交问题反馈')} <i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证 DOM 元素存在
|
||||
if (!this.elements.qrcode || !this.elements.esimInfo) {
|
||||
console.error('[Giffgaff] DOM elements not found: qrcode or esimInfo');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Giffgaff] Displaying eSIM result, LPA length:', state.lpaString.length);
|
||||
|
||||
// 正常流程:先显示 LPA 信息(确保用户至少能看到文本),再生成二维码
|
||||
// 调整顺序:LPA 文本优先,二维码可以慢慢加载
|
||||
this.elements.esimInfo.innerHTML = `
|
||||
<div class="mb-3">
|
||||
<h5 class="text-primary">${tl('LPA字符串')}</h5>
|
||||
<p class="text-break"><small>${state.lpaString}</small></p>
|
||||
@@ -559,30 +602,63 @@ export class UIController {
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 绑定按钮事件
|
||||
const copyBtn = document.getElementById('copyLpaBtn');
|
||||
const downloadBtn = document.getElementById('downloadQrBtn');
|
||||
// 绑定按钮事件
|
||||
const copyBtn = document.getElementById('copyLpaBtn');
|
||||
const downloadBtn = document.getElementById('downloadQrBtn');
|
||||
|
||||
if (copyBtn) {
|
||||
copyBtn.addEventListener('click', () => {
|
||||
if (window.copyLPAString) {
|
||||
window.copyLPAString(state.lpaString, copyBtn);
|
||||
}
|
||||
});
|
||||
if (copyBtn) {
|
||||
copyBtn.addEventListener('click', () => {
|
||||
if (window.copyLPAString) {
|
||||
window.copyLPAString(state.lpaString, copyBtn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (downloadBtn) {
|
||||
downloadBtn.addEventListener('click', () => {
|
||||
const img = this.elements.qrcode.querySelector('img');
|
||||
if (img && window.downloadQRCode) {
|
||||
window.downloadQRCode(img.src, 'giffgaff_esim_qrcode.png');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 生成二维码(放在最后,即使失败也不影响 LPA 文本显示)
|
||||
try {
|
||||
this.generateQRCode(state.lpaString);
|
||||
} catch (error) {
|
||||
console.error('[Giffgaff] generateQRCode failed:', error);
|
||||
this.elements.qrcode.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
二维码生成失败,请使用上方 LPA 字符串手动激活
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.elements.resultContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
// 最外层错误捕获:确保即使出现未预期异常,用户也能看到错误提示
|
||||
console.error('[Giffgaff] showESimResult failed:', error);
|
||||
this.elements.qrcode.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
显示 eSIM 信息时发生错误
|
||||
</div>
|
||||
`;
|
||||
this.elements.esimInfo.innerHTML = `
|
||||
<div class="alert alert-warning">
|
||||
<p><strong>错误详情:</strong> ${error.message}</p>
|
||||
<p class="mb-0">
|
||||
<small>
|
||||
请刷新页面重试,或按 F12 → Application → Session Storage → giffgaff_session 手动获取 lpaString
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (downloadBtn) {
|
||||
downloadBtn.addEventListener('click', () => {
|
||||
const img = this.elements.qrcode.querySelector('img');
|
||||
if (img && window.downloadQRCode) {
|
||||
window.downloadQRCode(img.src, 'giffgaff_esim_qrcode.png');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.elements.resultContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -413,6 +413,7 @@ export const TRANSLATIONS = {
|
||||
'giffgaff.app.status.tokenFetchedSuccess': 'eSIM下载代码获取成功',
|
||||
'giffgaff.app.status.oauthOpened': '登录页面已打开,请完成登录后复制回调URL',
|
||||
'giffgaff.app.error.tokenFailed': '获取eSIM下载代码失败:{message}',
|
||||
'giffgaff.app.error.lpaStringMissing': '获取 LPA 字符串失败,请稍后重试或联系支持',
|
||||
'giffgaff.app.status.lpaFetchedOnce': '已成功获取到 eSIM 二维码/LPA(为安全起见仅显示一次)',
|
||||
'giffgaff.app.prompt.clearAfterLpa': '已成功获取到 eSIM 二维码/LPA。是否立即清空会话并重置?',
|
||||
'giffgaff.app.prompt.resumeEsim': '检测到已有激活码/SSN,是否继续完成eSIM激活?',
|
||||
@@ -704,6 +705,7 @@ export const TRANSLATIONS = {
|
||||
'giffgaff.app.status.tokenFetchedSuccess': 'eSIM download token retrieved.',
|
||||
'giffgaff.app.status.oauthOpened': 'Login page opened. Complete the sign-in and copy the callback URL.',
|
||||
'giffgaff.app.error.tokenFailed': 'Failed to fetch download token: {message}',
|
||||
'giffgaff.app.error.lpaStringMissing': 'Failed to retrieve LPA string. Please try again later or contact support.',
|
||||
'giffgaff.app.status.lpaFetchedOnce': 'eSIM QR/LPA retrieved (displayed once for security).',
|
||||
'giffgaff.app.prompt.clearAfterLpa': 'eSIM QR/LPA already retrieved. Clear session and reset now?',
|
||||
'giffgaff.app.prompt.resumeEsim': 'Detected existing activation code/SSN. Continue eSIM activation?',
|
||||
|
||||
159
tests/giffgaff/session-restore-lpa.test.js
Normal file
159
tests/giffgaff/session-restore-lpa.test.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Giffgaff Session 恢复 LPA 显示测试
|
||||
* 修复 Issue #75:刷新页面后不显示二维码和 LPA 信息
|
||||
*/
|
||||
|
||||
describe('Giffgaff Session Restore - LPA Display', () => {
|
||||
let mockStateManager;
|
||||
let mockUIController;
|
||||
let mockState;
|
||||
|
||||
beforeEach(() => {
|
||||
// 清理 DOM
|
||||
document.body.innerHTML = `
|
||||
<div id="resultContainer"></div>
|
||||
<div id="qrcode"></div>
|
||||
<div id="esimInfo"></div>
|
||||
<div id="tokenStatus"></div>
|
||||
`;
|
||||
|
||||
// Mock state with lpaString
|
||||
mockState = {
|
||||
accessToken: 'mock-token',
|
||||
emailSignature: 'mock-signature',
|
||||
memberId: 'mock-member-id',
|
||||
esimSSN: '8944123456789012345',
|
||||
esimActivationCode: 'ABC123',
|
||||
lpaString: 'LPA:1$example.com$activation-code',
|
||||
currentStep: 5
|
||||
};
|
||||
|
||||
// Mock StateManager
|
||||
mockStateManager = {
|
||||
getState: jest.fn(() => mockState),
|
||||
loadSession: jest.fn(() => true),
|
||||
subscribe: jest.fn(),
|
||||
getCookie: jest.fn(() => null),
|
||||
get: jest.fn((key) => mockState[key])
|
||||
};
|
||||
|
||||
// Mock UIController
|
||||
mockUIController = {
|
||||
elements: {
|
||||
resultContainer: document.getElementById('resultContainer'),
|
||||
qrcode: document.getElementById('qrcode'),
|
||||
esimInfo: document.getElementById('esimInfo'),
|
||||
tokenStatus: document.getElementById('tokenStatus')
|
||||
},
|
||||
showESimResult: jest.fn(),
|
||||
showStatus: jest.fn(),
|
||||
showSection: jest.fn(),
|
||||
updateStatusPanel: jest.fn()
|
||||
};
|
||||
});
|
||||
|
||||
test('应该在 session 恢复时显示 LPA 和二维码', () => {
|
||||
// 验证初始状态
|
||||
expect(mockState.lpaString).toBeTruthy();
|
||||
expect(mockState.esimSSN).toBeTruthy();
|
||||
|
||||
// 模拟 handleSessionRestore 的核心逻辑
|
||||
if (mockState.lpaString) {
|
||||
mockUIController.showESimResult();
|
||||
}
|
||||
|
||||
// 验证 showESimResult 被调用
|
||||
expect(mockUIController.showESimResult).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('showESimResult 应该处理空 lpaString 的情况', () => {
|
||||
// 模拟 lpaString 为空
|
||||
const emptyState = { ...mockState, lpaString: '' };
|
||||
mockStateManager.getState = jest.fn(() => emptyState);
|
||||
|
||||
const resultContainer = document.getElementById('resultContainer');
|
||||
const qrcode = document.getElementById('qrcode');
|
||||
const esimInfo = document.getElementById('esimInfo');
|
||||
|
||||
// 模拟 showESimResult 的防御性逻辑
|
||||
const state = mockStateManager.getState();
|
||||
resultContainer.classList.add('active');
|
||||
|
||||
if (!state.lpaString) {
|
||||
qrcode.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
获取 LPA 字符串失败
|
||||
</div>
|
||||
`;
|
||||
esimInfo.innerHTML = `
|
||||
<div class="alert alert-warning">
|
||||
<p><strong>可能原因:</strong></p>
|
||||
<ul class="mb-0">
|
||||
<li>后端轮询超时或失败</li>
|
||||
<li>GraphQL 响应异常</li>
|
||||
<li>网络连接中断</li>
|
||||
<li>eSIM 状态异常</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 验证错误提示显示
|
||||
expect(resultContainer.classList.contains('active')).toBe(true);
|
||||
expect(qrcode.innerHTML).toContain('获取 LPA 字符串失败');
|
||||
expect(esimInfo.innerHTML).toContain('可能原因');
|
||||
});
|
||||
|
||||
test('showESimResult 应该正常生成二维码(有 lpaString)', () => {
|
||||
const resultContainer = document.getElementById('resultContainer');
|
||||
const qrcode = document.getElementById('qrcode');
|
||||
|
||||
// 模拟 generateQRCode 的简化逻辑
|
||||
const state = mockStateManager.getState();
|
||||
resultContainer.classList.add('active');
|
||||
|
||||
if (state.lpaString) {
|
||||
const img = document.createElement('img');
|
||||
img.src = `https://qrcode.show/${encodeURIComponent(state.lpaString)}`;
|
||||
img.alt = 'eSIM二维码';
|
||||
qrcode.appendChild(img);
|
||||
}
|
||||
|
||||
// 验证二维码生成
|
||||
expect(resultContainer.classList.contains('active')).toBe(true);
|
||||
expect(qrcode.querySelector('img')).toBeTruthy();
|
||||
expect(qrcode.querySelector('img').src).toContain('qrcode.show');
|
||||
});
|
||||
|
||||
test('session 恢复应该处理不完整状态(无 SSN)', () => {
|
||||
// 模拟状态不一致:有 lpaString 但无 SSN
|
||||
const inconsistentState = {
|
||||
...mockState,
|
||||
esimSSN: '', // SSN 缺失
|
||||
lpaString: 'LPA:1$example.com$activation-code'
|
||||
};
|
||||
mockStateManager.getState = jest.fn(() => inconsistentState);
|
||||
|
||||
// 模拟状态降级逻辑
|
||||
let targetStep = 5;
|
||||
if (inconsistentState.lpaString && !inconsistentState.esimSSN) {
|
||||
console.warn('[Test] State inconsistency detected');
|
||||
targetStep = 4; // 降级到 step 4
|
||||
}
|
||||
|
||||
// 验证降级逻辑
|
||||
expect(targetStep).toBe(4);
|
||||
});
|
||||
|
||||
test('session 恢复应该在 500ms 后显示二维码(模拟延迟)', (done) => {
|
||||
const showResult = jest.fn();
|
||||
|
||||
// 模拟延迟调用
|
||||
setTimeout(() => {
|
||||
showResult();
|
||||
expect(showResult).toHaveBeenCalled();
|
||||
done();
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user