(input: OpenOAuthPopupInput<TAuth>)
| 94 | * `onOpenFailed` on the next microtask and returns a no-op teardown. |
| 95 | */ |
| 96 | export const openOAuthPopup = <TAuth>(input: OpenOAuthPopupInput<TAuth>): (() => void) => { |
| 97 | if (!isHttpPopupUrl(input.url)) { |
| 98 | queueMicrotask(() => input.onOpenFailed?.()); |
| 99 | return () => {}; |
| 100 | } |
| 101 | |
| 102 | let settled = false; |
| 103 | let pollHandle: ReturnType<typeof setInterval> | null = null; |
| 104 | const channel = |
| 105 | typeof BroadcastChannel !== "undefined" ? new BroadcastChannel(input.channelName) : null; |
| 106 | |
| 107 | const onMessage = (event: MessageEvent) => { |
| 108 | if (event.origin !== window.location.origin) return; |
| 109 | handleResult(event.data); |
| 110 | }; |
| 111 | |
| 112 | // localStorage `storage` events are the reliable same-origin completion path: |
| 113 | // they fire on the opener when the (same-origin) callback page writes, survive |
| 114 | // the provider's COOP severing `window.opener`, and aren't lost to the popup's |
| 115 | // auto-close (unlike a raced BroadcastChannel). The callback writes the result |
| 116 | // under `channelName`; we read it, clean up, and settle. |
| 117 | const onStorage = (event: StorageEvent) => { |
| 118 | if (event.key !== input.channelName || event.newValue === null) return; |
| 119 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: JSON.parse of a foreign storage value can throw |
| 120 | try { |
| 121 | // oxlint-disable-next-line executor/no-json-parse -- boundary: browser-only helper, no Effect runtime; parsing a same-origin localStorage signal |
| 122 | const data: unknown = JSON.parse(event.newValue); |
| 123 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: localStorage can throw (private mode / disabled) |
| 124 | try { |
| 125 | window.localStorage.removeItem(input.channelName); |
| 126 | } catch { |
| 127 | // best-effort cleanup |
| 128 | } |
| 129 | handleResult(data); |
| 130 | } catch { |
| 131 | // Malformed value — ignore; another channel may still deliver. |
| 132 | } |
| 133 | }; |
| 134 | |
| 135 | const stopPolling = () => { |
| 136 | if (pollHandle !== null) { |
| 137 | clearInterval(pollHandle); |
| 138 | pollHandle = null; |
| 139 | } |
| 140 | }; |
| 141 | |
| 142 | /** Close the popup window if it's still open. Swallows cross-origin errors. */ |
| 143 | const closePopup = (popup: Window | null) => { |
| 144 | if (!popup) return; |
| 145 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: cross-origin popup state can throw and cleanup is best-effort |
| 146 | try { |
| 147 | if (!popup.closed) popup.close(); |
| 148 | } catch { |
| 149 | // Cross-origin access can throw; safe to ignore. |
| 150 | } |
| 151 | }; |
| 152 | |
| 153 | const settle = () => { |
no test coverage detected