+21
-5
CHANGELOG.md
+21
-5
CHANGELOG.md
···
2
2
3
3
All notable changes to this project will be documented in this file.
4
4
5
+
## [2.5.1] - 2025-01-09
6
+
7
+
### Fixed
8
+
9
+
- **PWA OAuth localStorage fallback**: Added localStorage-based communication as
10
+
a fallback for PWA OAuth flows. When navigating through external OAuth
11
+
providers (like bsky.social), the `window.opener` reference is lost, causing
12
+
`postMessage` to fail. The callback now stores the result in localStorage,
13
+
which the opener can read via the `storage` event or by checking localStorage
14
+
when the popup closes.
15
+
5
16
## [2.5.0] - 2025-01-09
6
17
7
18
### Added
···
23
34
// PWA detects standalone mode and opens OAuth in popup
24
35
const popup = window.open("/login?handle=user.bsky&pwa=true", "oauth-popup");
25
36
26
-
// Listen for postMessage from popup
27
-
window.addEventListener("message", (event) => {
28
-
if (event.data.type === "oauth-callback" && event.data.success) {
37
+
// Listen for both postMessage and localStorage
38
+
window.addEventListener("message", handleOAuthResult);
39
+
window.addEventListener("storage", (e) => {
40
+
if (e.key === "pwa-oauth-result") handleOAuthResult(JSON.parse(e.newValue));
41
+
});
42
+
43
+
function handleOAuthResult(data) {
44
+
if (data.type === "oauth-callback" && data.success) {
29
45
// Session cookie is set, reload to pick it up
30
46
location.reload();
31
47
}
32
-
});
48
+
}
33
49
```
34
50
35
51
### Security
36
52
37
53
- PWA callbacks still set the session cookie for API authentication
38
54
- The `postMessage` only sends `did` and `handle` (no tokens)
39
-
- Fallback redirect to home page if `window.opener` is unavailable
55
+
- localStorage data is cleared after successful read
40
56
41
57
## [2.4.0] - 2025-12-14
42
58
+1
-1
deno.json
+1
-1
deno.json
+35
-20
src/routes.ts
+35
-20
src/routes.ts
···
197
197
});
198
198
}
199
199
200
-
// PWA OAuth: return HTML page with postMessage
200
+
// PWA OAuth: return HTML page that signals completion via localStorage
201
+
// We use localStorage instead of postMessage because window.opener
202
+
// is lost after navigating through external OAuth providers
201
203
if (state.pwa) {
202
204
logger.info(`PWA OAuth complete for ${state.handle}`);
203
205
···
224
226
border-radius: 8px;
225
227
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
226
228
}
227
-
.spinner {
228
-
width: 24px;
229
-
height: 24px;
230
-
border: 3px solid #e0e0e0;
231
-
border-top-color: #FF6B6B;
229
+
.success-icon {
230
+
width: 48px;
231
+
height: 48px;
232
+
background: #10b981;
232
233
border-radius: 50%;
233
-
animation: spin 1s linear infinite;
234
+
display: flex;
235
+
align-items: center;
236
+
justify-content: center;
234
237
margin: 0 auto 1rem;
235
238
}
236
-
@keyframes spin {
237
-
to { transform: rotate(360deg); }
239
+
.success-icon svg {
240
+
width: 24px;
241
+
height: 24px;
242
+
fill: white;
238
243
}
239
244
</style>
240
245
</head>
241
246
<body>
242
247
<div class="message">
243
-
<div class="spinner"></div>
244
-
<p>Completing login...</p>
248
+
<div class="success-icon">
249
+
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
250
+
</div>
251
+
<p>Login successful!</p>
252
+
<p style="color: #666; font-size: 14px;">You can close this window.</p>
245
253
</div>
246
254
<script>
247
255
(function() {
256
+
// Store success data in localStorage for the opener to read
248
257
var data = {
249
258
type: 'oauth-callback',
250
259
success: true,
251
260
did: ${JSON.stringify(did)},
252
-
handle: ${JSON.stringify(state.handle)}
261
+
handle: ${JSON.stringify(state.handle)},
262
+
timestamp: Date.now()
253
263
};
264
+
localStorage.setItem('pwa-oauth-result', JSON.stringify(data));
254
265
255
-
// Send to opener (PWA window) if available
256
-
if (window.opener) {
257
-
window.opener.postMessage(data, '*');
258
-
// Close this popup after a short delay
259
-
setTimeout(function() { window.close(); }, 500);
260
-
} else {
261
-
// Fallback: redirect to home (cookie is set)
262
-
window.location.href = '/';
266
+
// Try postMessage first (works if opener is still available)
267
+
if (window.opener && !window.opener.closed) {
268
+
try {
269
+
window.opener.postMessage(data, '*');
270
+
} catch (e) {
271
+
// Ignore cross-origin errors
272
+
}
263
273
}
274
+
275
+
// Close popup after a short delay
276
+
setTimeout(function() {
277
+
window.close();
278
+
}, 1500);
264
279
})();
265
280
</script>
266
281
</body>