diff --git a/CLAUDE.md b/CLAUDE.md index aa9c41b..6dd71ed 100644 --- a/CLAUDE.md +++ b/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 | 文档清理与路径重构:移除未使用的新模块化版本,修复文档错链,精简重复文档 | diff --git a/src/giffgaff/js/giffgaff-app.js b/src/giffgaff/js/giffgaff-app.js index c755971..1106d76 100644 --- a/src/giffgaff/js/giffgaff-app.js +++ b/src/giffgaff/js/giffgaff-app.js @@ -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'), diff --git a/src/giffgaff/js/modules/ui-controller.js b/src/giffgaff/js/modules/ui-controller.js index da2d74f..5b78dd7 100644 --- a/src/giffgaff/js/modules/ui-controller.js +++ b/src/giffgaff/js/modules/ui-controller.js @@ -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 = ` +
+ + ${t('giffgaff.app.error.lpaStringMissing') || '获取 LPA 字符串失败'} +
+ `; + this.elements.esimInfo.innerHTML = ` +
+

${tl('可能原因')}:

+ +
+

+ + ${tl('请打开浏览器控制台(F12)查看详细日志')} +
+ + ${tl('提交问题反馈')} + +
+

+
+ `; + 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 = `
${tl('LPA字符串')}

${state.lpaString}

@@ -559,30 +602,63 @@ export class UIController {
`; - // 绑定按钮事件 - 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 = ` +
+ + 二维码生成失败,请使用上方 LPA 字符串手动激活 +
+ `; + } + + setTimeout(() => { + this.elements.resultContainer.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }, 500); + } catch (error) { + // 最外层错误捕获:确保即使出现未预期异常,用户也能看到错误提示 + console.error('[Giffgaff] showESimResult failed:', error); + this.elements.qrcode.innerHTML = ` +
+ + 显示 eSIM 信息时发生错误 +
+ `; + this.elements.esimInfo.innerHTML = ` +
+

错误详情: ${error.message}

+

+ + 请刷新页面重试,或按 F12 → Application → Session Storage → giffgaff_session 手动获取 lpaString + +

+
+ `; } - - 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); } /** diff --git a/src/js/modules/i18n-data.js b/src/js/modules/i18n-data.js index bccb870..24bd0e2 100644 --- a/src/js/modules/i18n-data.js +++ b/src/js/modules/i18n-data.js @@ -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?', diff --git a/tests/giffgaff/session-restore-lpa.test.js b/tests/giffgaff/session-restore-lpa.test.js new file mode 100644 index 0000000..91f3b87 --- /dev/null +++ b/tests/giffgaff/session-restore-lpa.test.js @@ -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 = ` +
+
+
+
+ `; + + // 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 = ` +
+ + 获取 LPA 字符串失败 +
+ `; + esimInfo.innerHTML = ` +
+

可能原因:

+ +
+ `; + } + + // 验证错误提示显示 + 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); + }); +});