-
Notifications
You must be signed in to change notification settings - Fork 1
PE-6102 | Show session info in extension popup #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
90ff669
PE-6102 | add extension popup showing session info
mihoward21 fe5b42b
PE-6102 | relay session info via content script instead of externally…
mihoward21 9a81ee8
PE-6102 | show only the user in the popup; move markup into templates
mihoward21 048561d
PE-6102 | expire stored session info after 7 days
mihoward21 ecbe684
PE-6102 | always show the welcome message; signed-in user shown below it
mihoward21 c439dd8
PE-6102 | move the signed-in card above the welcome message
mihoward21 5a70843
PE-6102 | welcome copy: two sentences instead of an em dash
mihoward21 0ec423d
PE-6102 | drop the welcome heading
mihoward21 532c9e3
PE-6102 | rework welcome copy
mihoward21 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,13 +2,45 @@ import { | |
| RPC_STORAGE_MESSAGE_TYPE, | ||
| consumeRpcPayload, | ||
| } from "@/utils/rpc-storage"; | ||
| import { | ||
| SESSION_INFO_STORAGE_KEY, | ||
| isSessionInfoMessage, | ||
| sanitizeSessionInfo, | ||
| } from "@/utils/session-info"; | ||
| import { getHost } from "../../utils"; | ||
|
|
||
| export default defineBackground(() => { | ||
| RpcStorageBridge.register(); | ||
| SessionInfoBridge.register(); | ||
| ContextMenu.create(); | ||
| }); | ||
|
|
||
| const allowedOrigin = new URL(getHost(import.meta.env.MODE)).origin; | ||
|
|
||
| type MessageSender = Parameters< | ||
| Parameters<typeof browser.runtime.onMessageExternal.addListener>[0] | ||
| >[1]; | ||
|
|
||
| function isAllowedSender(sender: MessageSender) { | ||
| const senderOrigin = getSenderOrigin(sender); | ||
| if (!senderOrigin) return false; | ||
| return senderOrigin === allowedOrigin; | ||
| } | ||
|
|
||
| function getSenderOrigin(sender: MessageSender) { | ||
| if (typeof sender.origin === "string" && sender.origin.length > 0) { | ||
| return sender.origin; | ||
| } | ||
| if (typeof sender.url === "string" && sender.url.length > 0) { | ||
| try { | ||
| return new URL(sender.url).origin; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| namespace ContextMenu { | ||
| export function create() { | ||
| browser.contextMenus.create({ | ||
|
|
@@ -27,11 +59,6 @@ namespace ContextMenu { | |
| } | ||
|
|
||
| namespace RpcStorageBridge { | ||
| const allowedOrigin = new URL(getHost(import.meta.env.MODE)).origin; | ||
| type MessageSender = Parameters< | ||
| Parameters<typeof browser.runtime.onMessageExternal.addListener>[0] | ||
| >[1]; | ||
|
|
||
| export function register() { | ||
| browser.runtime.onMessageExternal.addListener(handleExternalMessage); | ||
| } | ||
|
|
@@ -66,29 +93,58 @@ namespace RpcStorageBridge { | |
| ); | ||
| } | ||
|
|
||
| function isAllowedSender(sender: MessageSender) { | ||
| const senderOrigin = getSenderOrigin(sender); | ||
| if (!senderOrigin) return false; | ||
| return senderOrigin === allowedOrigin; | ||
| async function fetchStoredPayload(token: string) { | ||
| const value = await consumeRpcPayload(token); | ||
| if (!value) return { ok: false }; | ||
| return { ok: true, value }; | ||
| } | ||
| } | ||
|
|
||
| function getSenderOrigin(sender: MessageSender) { | ||
| if (typeof sender.origin === "string" && sender.origin.length > 0) { | ||
| return sender.origin; | ||
| } | ||
| if (typeof sender.url === "string" && sender.url.length > 0) { | ||
| try { | ||
| return new URL(sender.url).origin; | ||
| } catch { | ||
| return null; | ||
| } | ||
| // Lets the Splits Teams app keep the popup's session display in sync. The app | ||
| // posts `{ type: "splits-connect:setSessionInfo", sessionInfo }` to its own | ||
| // window whenever auth state resolves, and `sessionInfo: null` when it | ||
| // resolves signed out; the content script relays it here. Sender pages are | ||
| // re-checked against the Teams origin before anything is stored. | ||
| // | ||
| // Spoofing is accepted by design: any script running on the Teams origin | ||
| // (third-party JS, other extensions' content scripts) can post this message | ||
| // and repaint the popup's display strings. The payload is sanitized below | ||
| // and the popup renders text only, so there is no injection path and nothing | ||
| // privileged to reach — and anything running on that origin can already read | ||
| // the real session from the page. | ||
| namespace SessionInfoBridge { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here is the net new stuff |
||
| export function register() { | ||
| browser.runtime.onMessage.addListener(handleMessage); | ||
| } | ||
|
|
||
| function handleMessage( | ||
| message: unknown, | ||
| sender: MessageSender, | ||
| sendResponse: (response: unknown) => void | ||
| ) { | ||
| if (!isSessionInfoMessage(message)) return undefined; | ||
| if (!isAllowedSender(sender)) { | ||
| sendResponse({ ok: false }); | ||
| return undefined; | ||
| } | ||
| return null; | ||
| const sessionInfo = sanitizeSessionInfo( | ||
| (message as { sessionInfo?: unknown }).sessionInfo | ||
| ); | ||
| void persistSessionInfo(sessionInfo) | ||
| .then(() => sendResponse({ ok: true })) | ||
| .catch(() => sendResponse({ ok: false })); | ||
| return true; | ||
| } | ||
|
|
||
| async function fetchStoredPayload(token: string) { | ||
| const value = await consumeRpcPayload(token); | ||
| if (!value) return { ok: false }; | ||
| return { ok: true, value }; | ||
| async function persistSessionInfo( | ||
| sessionInfo: ReturnType<typeof sanitizeSessionInfo> | ||
| ) { | ||
| if (!sessionInfo) { | ||
| await browser.storage.local.remove(SESSION_INFO_STORAGE_KEY); | ||
| return; | ||
| } | ||
| await browser.storage.local.set({ | ||
| [SESSION_INFO_STORAGE_KEY]: sessionInfo, | ||
| }); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| <title>Splits Connect</title> | ||
| <link rel="stylesheet" href="./style.css" /> | ||
| </head> | ||
| <body> | ||
| <header class="header"> | ||
| <img class="header-logo" src="/logo.svg" alt="" /> | ||
| <span class="header-title">Splits Connect</span> | ||
| </header> | ||
| <main class="content"> | ||
| <section id="session" class="session" hidden></section> | ||
| <p class="welcome-text"> | ||
| This extension allows you to connect Splits to third-party | ||
| applications. There is nothing you need to do here. If Splits doesn't | ||
| show up in the application's wallet list, or you have any other | ||
| questions, please reach out to | ||
| <a href="mailto:support@splits.org">support@splits.org</a>. | ||
| </p> | ||
| </main> | ||
| <footer class="footer"> | ||
| <a | ||
| href="https://www.notion.so/splits/Connecting-to-other-apps-29af7c3c8eff802fb110e7b9aa316488?source=copy_link" | ||
| target="_blank" | ||
| rel="noreferrer" | ||
| >Learn more about Splits Connect ↗</a | ||
| > | ||
| </footer> | ||
|
|
||
| <template id="signed-in"> | ||
| <h2 class="session-label">Signed in as</h2> | ||
| <div class="user"> | ||
| <div class="avatar avatar-fallback"></div> | ||
| <img class="avatar avatar-image" alt="" hidden /> | ||
| <div class="user-details"> | ||
| <div class="user-name"></div> | ||
| <div class="user-email"></div> | ||
| </div> | ||
| </div> | ||
| </template> | ||
|
|
||
| <script type="module" src="./main.ts"></script> | ||
| </body> | ||
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import { | ||
| SESSION_INFO_STORAGE_KEY, | ||
| isSessionInfoFresh, | ||
| type SessionInfo, | ||
| } from "@/utils/session-info"; | ||
|
|
||
| const session = document.getElementById("session"); | ||
| if (session) { | ||
| void readSessionInfo().then((sessionInfo) => render(session, sessionInfo)); | ||
| browser.storage.local.onChanged.addListener((changes) => { | ||
| if (!(SESSION_INFO_STORAGE_KEY in changes)) return; | ||
| const next = changes[SESSION_INFO_STORAGE_KEY]?.newValue as | ||
| | SessionInfo | ||
| | undefined; | ||
| render(session, next ?? null); | ||
| }); | ||
| } | ||
|
|
||
| async function readSessionInfo(): Promise<SessionInfo | null> { | ||
| const stored = await browser.storage.local.get(SESSION_INFO_STORAGE_KEY); | ||
| return (stored[SESSION_INFO_STORAGE_KEY] as SessionInfo | undefined) ?? null; | ||
| } | ||
|
|
||
| // The welcome message is static in the HTML and always visible; this only | ||
| // fills (or hides) the "Signed in as" card below it. | ||
| function render(target: HTMLElement, sessionInfo: SessionInfo | null) { | ||
| const fresh = | ||
| sessionInfo && isSessionInfoFresh(sessionInfo) ? sessionInfo : null; | ||
| if (!fresh) { | ||
| target.replaceChildren(); | ||
| target.hidden = true; | ||
| return; | ||
| } | ||
| target.replaceChildren(renderUser(fresh.user)); | ||
| target.hidden = false; | ||
| } | ||
|
|
||
| function renderUser(user: SessionInfo["user"]) { | ||
| const template = document.getElementById("signed-in") as HTMLTemplateElement; | ||
| const view = template.content.cloneNode(true) as DocumentFragment; | ||
|
|
||
| setText(view, ".user-name", user.name ?? user.email ?? ""); | ||
| if (user.name && user.email) setText(view, ".user-email", user.email); | ||
| else view.querySelector(".user-email")?.remove(); | ||
|
|
||
| const source = user.name ?? user.email ?? ""; | ||
| setText(view, ".avatar-fallback", source.slice(0, 1).toUpperCase()); | ||
|
|
||
| // The fallback initial shows until the avatar image actually loads; a | ||
| // broken or missing URL never swaps it out. | ||
| const image = view.querySelector<HTMLImageElement>(".avatar-image"); | ||
| const fallback = view.querySelector<HTMLElement>(".avatar-fallback"); | ||
| if (image && fallback && user.avatarUrl) { | ||
| image.addEventListener( | ||
| "load", | ||
| () => { | ||
| image.hidden = false; | ||
| fallback.remove(); | ||
| }, | ||
| { once: true } | ||
| ); | ||
| image.src = user.avatarUrl; | ||
| } | ||
|
|
||
| return view; | ||
| } | ||
|
|
||
| function setText(view: DocumentFragment, selector: string, text: string) { | ||
| const element = view.querySelector(selector); | ||
| if (element) element.textContent = text; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mostly just reshuffling existing code here