( payload: OAuthPopupResult<TAuth>, channelName: string, )
| 75 | * itself. |
| 76 | */ |
| 77 | export const popupDocument = <TAuth>( |
| 78 | payload: OAuthPopupResult<TAuth>, |
| 79 | channelName: string, |
| 80 | ): string => { |
| 81 | const serialized = serializeForScript(payload); |
| 82 | const title = payload.ok ? "Connected" : "Connection failed"; |
| 83 | const message = payload.ok |
| 84 | ? "Authentication complete. This window will close automatically." |
| 85 | : payload.error; |
| 86 | const details = |
| 87 | !payload.ok && payload.errorDetails && payload.errorDetails !== payload.error |
| 88 | ? payload.errorDetails |
| 89 | : undefined; |
| 90 | const statusColor = payload.ok ? "#22c55e" : "#ef4444"; |
| 91 | const icon = payload.ok |
| 92 | ? '<path d="M6 10l3 3 5-6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' |
| 93 | : '<path d="M7 7l6 6M13 7l-6 6" stroke="white" stroke-width="2" stroke-linecap="round"/>'; |
| 94 | const escapedChannel = escapeHtml(channelName); |
| 95 | const detailsHtml = details |
| 96 | ? `<details style="margin-top:16px;text-align:left"><summary style="cursor:pointer;font-size:12px;color:#888;user-select:none">Details</summary><pre style="white-space:pre-wrap;word-break:break-word;font-size:11px;line-height:1.5;color:#52525b;background:#f4f4f5;padding:8px;border-radius:4px;margin:8px 0 0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace">${escapeHtml(details)}</pre></details>` |
| 97 | : ""; |
| 98 | |
| 99 | return `<!doctype html> |
| 100 | <html lang="en"> |
| 101 | <head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>${escapeHtml(title)}</title></head> |
| 102 | <body style="font-family:system-ui,-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#fafafa;color:#111"> |
| 103 | <style>@media(prefers-color-scheme:dark){body{background:#09090b!important;color:#fafafa!important}p{color:#a1a1aa!important}pre{background:#18181b!important;color:#a1a1aa!important}}</style> |
| 104 | <main style="text-align:center;max-width:420px;padding:24px"> |
| 105 | <div style="width:40px;height:40px;border-radius:50%;background:${statusColor};margin:0 auto 16px;display:flex;align-items:center;justify-content:center"> |
| 106 | <svg width="20" height="20" viewBox="0 0 20 20" fill="none">${icon}</svg> |
| 107 | </div> |
| 108 | <h1 style="margin:0 0 8px;font-size:18px;font-weight:600">${escapeHtml(title)}</h1> |
| 109 | <p style="margin:0;font-size:14px;color:#666;line-height:1.5">${escapeHtml(message)}</p> |
| 110 | ${detailsHtml} |
| 111 | </main> |
| 112 | <script> |
| 113 | (()=>{const p=${serialized}; |
| 114 | // Three same-origin completion channels, each isolated so one failing doesn't |
| 115 | // block the others. postMessage is severed when the provider's consent page set |
| 116 | // COOP (window.opener becomes null), and BroadcastChannel can be partitioned/ |
| 117 | // raced by the auto-close — so localStorage (a 'storage' event on the opener) is |
| 118 | // the reliable fallback. The opener settles on whichever lands first. |
| 119 | try{if(window.opener)window.opener.postMessage(p,window.location.origin)}catch(e){} |
| 120 | try{if("BroadcastChannel"in window){const c=new BroadcastChannel("${escapedChannel}");c.postMessage(p);setTimeout(()=>c.close(),100)}}catch(e){} |
| 121 | try{localStorage.setItem("${escapedChannel}",JSON.stringify(p))}catch(e){} |
| 122 | if(p.ok)setTimeout(()=>window.close(),400);})(); |
| 123 | </script> |
| 124 | </body></html>`; |
| 125 | }; |
| 126 | |
| 127 | // --------------------------------------------------------------------------- |
| 128 | // Callback wrapper — turns a completeOAuth Effect into a popup HTML string. |
no test coverage detected