From c05003fb26c50108503a590258394b2636253f3d Mon Sep 17 00:00:00 2001
From: Heath Stewart
Date: Sat, 28 Feb 2026 01:55:46 -0800
Subject: [PATCH] Check existing subs and offer to unsubscribe
---
docs/src/routes/subscribe.ts | 118 +++++++++++++++---
.../cli/src/components/sequoia-subscribe.js | 92 +++++++-------
2 files changed, 147 insertions(+), 63 deletions(-)
diff --git a/docs/src/routes/subscribe.ts b/docs/src/routes/subscribe.ts
index 45170be..7e26d69 100644
--- a/docs/src/routes/subscribe.ts
+++ b/docs/src/routes/subscribe.ts
@@ -154,8 +154,48 @@ subscribe.post("/", async (c) => {
subscribe.get("/", async (c) => {
const publicationUri = c.req.query("publicationUri");
+ const action = c.req.query("action");
+ const wantsJson = c.req.header("accept")?.includes("application/json");
+
+ // JSON path: subscription status check for the web component.
+ if (wantsJson) {
+ if (action && action !== "unsubscribe") {
+ return c.json({ error: `Unsupported action: ${action}` }, 400);
+ }
+ if (!publicationUri || !publicationUri.startsWith("at://")) {
+ return c.json({ error: "Missing or invalid publicationUri" }, 400);
+ }
+ const did = getSessionDid(c);
+ if (!did) {
+ return c.json({ authenticated: false }, 401);
+ }
+ try {
+ const client = createOAuthClient(
+ c.env.SEQUOIA_SESSIONS,
+ c.env.CLIENT_URL,
+ );
+ const session = await client.restore(did);
+ const agent = new Agent(session);
+ const recordUri = await findExistingSubscription(
+ agent,
+ did,
+ publicationUri,
+ );
+ return recordUri
+ ? c.json({ subscribed: true, recordUri })
+ : c.json({ subscribed: false });
+ } catch {
+ return c.json({ authenticated: false }, 401);
+ }
+ }
+
+ // HTML path: full-page subscribe/unsubscribe flow.
const styleHref = await getVocsStyleHref(c.env.ASSETS, c.req.url);
+ if (action && action !== "unsubscribe") {
+ return c.html(renderError(`Unsupported action: ${action}`, styleHref), 400);
+ }
+
if (!publicationUri || !publicationUri.startsWith("at://")) {
return c.html(
renderError("Missing or invalid publication URI.", styleHref),
@@ -172,7 +212,9 @@ subscribe.get("/", async (c) => {
const did = getSessionDid(c);
if (!did) {
- return c.html(renderHandleForm(publicationUri, styleHref, returnTo));
+ return c.html(
+ renderHandleForm(publicationUri, styleHref, returnTo, undefined, action),
+ );
}
try {
@@ -180,6 +222,34 @@ subscribe.get("/", async (c) => {
const session = await client.restore(did);
const agent = new Agent(session);
+ if (action === "unsubscribe") {
+ const existingUri = await findExistingSubscription(
+ agent,
+ did,
+ publicationUri,
+ );
+ if (existingUri) {
+ const rkey = existingUri.split("/").pop()!;
+ await agent.com.atproto.repo.deleteRecord({
+ repo: did,
+ collection: COLLECTION,
+ rkey,
+ });
+ }
+ return c.html(
+ renderSuccess(
+ publicationUri,
+ null,
+ "Unsubscribed ✓",
+ existingUri
+ ? "You've successfully unsubscribed!"
+ : "You weren't subscribed to this publication.",
+ styleHref,
+ returnTo,
+ ),
+ );
+ }
+
const existingUri = await findExistingSubscription(
agent,
did,
@@ -187,7 +257,14 @@ subscribe.get("/", async (c) => {
);
if (existingUri) {
return c.html(
- renderSuccess(publicationUri, existingUri, true, styleHref, returnTo),
+ renderSuccess(
+ publicationUri,
+ existingUri,
+ "Subscribed ✓",
+ "You're already subscribed to this publication.",
+ styleHref,
+ returnTo,
+ ),
);
}
@@ -204,7 +281,8 @@ subscribe.get("/", async (c) => {
renderSuccess(
publicationUri,
result.data.uri,
- false,
+ "Subscribed ✓",
+ "You've successfully subscribed!",
styleHref,
returnTo,
),
@@ -218,6 +296,7 @@ subscribe.get("/", async (c) => {
styleHref,
returnTo,
"Session expired. Please sign in again.",
+ action,
),
);
}
@@ -235,6 +314,7 @@ subscribe.post("/login", async (c) => {
const handle = (body["handle"] as string | undefined)?.trim();
const publicationUri = body["publicationUri"] as string | undefined;
const formReturnTo = (body["returnTo"] as string | undefined) || undefined;
+ const formAction = (body["action"] as string | undefined) || undefined;
if (!handle || !publicationUri) {
const styleHref = await getVocsStyleHref(c.env.ASSETS, c.req.url);
@@ -246,6 +326,7 @@ subscribe.post("/login", async (c) => {
const returnTo =
`${c.env.CLIENT_URL}/subscribe?publicationUri=${encodeURIComponent(publicationUri)}` +
+ (formAction ? `&action=${encodeURIComponent(formAction)}` : "") +
(formReturnTo ? `&returnTo=${encodeURIComponent(formReturnTo)}` : "");
setReturnToCookie(c, returnTo, c.env.CLIENT_URL);
@@ -263,6 +344,7 @@ function renderHandleForm(
styleHref: string,
returnTo?: string,
error?: string,
+ action?: string,
): string {
const errorHtml = error
? `${escapeHtml(error)}
`
@@ -270,6 +352,9 @@ function renderHandleForm(
const returnToInput = returnTo
? ``
: "";
+ const actionInput = action
+ ? ``
+ : "";
return page(
`
@@ -279,6 +364,7 @@ function renderHandleForm(
+ ? `Redirecting to ${escapedReturnTo} in ${REDIRECT_DELAY_SECONDS}\u00a0seconds\u2026
`
: "";
const headExtra = returnTo
- ? ``
+ ? ``
: "";
return page(
`
- Subscribed ✓
+ ${escapeHtml(heading)}
${msg}
${redirectHtml}
@@ -339,12 +423,16 @@ function renderSuccess(
-
+ ${
+ recordUri
+ ? `
| Record |
-
+
|
-
+ `
+ : ""
+ }
`,
diff --git a/packages/cli/src/components/sequoia-subscribe.js b/packages/cli/src/components/sequoia-subscribe.js
index 3d32c04..a0e5e84 100644
--- a/packages/cli/src/components/sequoia-subscribe.js
+++ b/packages/cli/src/components/sequoia-subscribe.js
@@ -79,14 +79,6 @@ const styles = `
flex-shrink: 0;
}
-.sequoia-subscribe-button--success {
- background: #16a34a;
-}
-
-.sequoia-subscribe-button--success:hover:not(:disabled) {
- background: color-mix(in srgb, #16a34a 85%, black);
-}
-
.sequoia-loading-spinner {
display: inline-block;
width: 1rem;
@@ -118,10 +110,6 @@ const BLUESKY_ICON = `
`;
-const CHECK_ICON = ``;
-
// ============================================================================
// AT Protocol Functions
// ============================================================================
@@ -178,6 +166,7 @@ class SequoiaSubscribe extends BaseElement {
wrapper.part = "container";
this.wrapper = wrapper;
+ this.subscribed = false;
this.state = { type: "idle" };
this.abortController = null;
this.render();
@@ -188,10 +177,7 @@ class SequoiaSubscribe extends BaseElement {
}
connectedCallback() {
- // Pre-check publication availability so hide="auto" can take effect
- if (!this.publicationUri) {
- this.checkPublication();
- }
+ this.checkPublication();
}
disconnectedCallback() {
@@ -199,12 +185,7 @@ class SequoiaSubscribe extends BaseElement {
}
attributeChangedCallback() {
- // Reset to idle if attributes change after an error or success
- if (
- this.state.type === "error" ||
- this.state.type === "subscribed" ||
- this.state.type === "no-publication"
- ) {
+ if (this.state.type === "error" || this.state.type === "no-publication") {
this.state = { type: "idle" };
}
this.render();
@@ -232,15 +213,44 @@ class SequoiaSubscribe extends BaseElement {
this.abortController = new AbortController();
try {
- await fetchPublicationUri();
+ const uri = this.publicationUri ?? (await fetchPublicationUri());
+ this.checkSubscription(uri);
} catch {
this.state = { type: "no-publication" };
this.render();
}
}
+ async checkSubscription(publicationUri) {
+ try {
+ const res = await fetch(
+ `${this.callbackUri}?publicationUri=${encodeURIComponent(publicationUri)}`,
+ {
+ headers: { Accept: "application/json" },
+ credentials: "include",
+ },
+ );
+ if (!res.ok) return;
+ const data = await res.json();
+ if (data.subscribed) {
+ this.subscribed = true;
+ this.render();
+ }
+ } catch {
+ // Ignore errors — show default subscribe button
+ }
+ }
+
async handleClick() {
- if (this.state.type === "loading" || this.state.type === "subscribed") {
+ if (this.state.type === "loading") {
+ return;
+ }
+
+ // Unsubscribe: redirect to full-page unsubscribe flow
+ if (this.subscribed) {
+ const publicationUri =
+ this.publicationUri ?? (await fetchPublicationUri());
+ window.location.href = `${this.callbackUri}?publicationUri=${encodeURIComponent(publicationUri)}&action=unsubscribe`;
return;
}
@@ -251,9 +261,6 @@ class SequoiaSubscribe extends BaseElement {
const publicationUri =
this.publicationUri ?? (await fetchPublicationUri());
- // POST to the callbackUri (e.g. https://sequoia.pub/subscribe).
- // If the server reports the user isn't authenticated it returns a
- // subscribeUrl for the full-page OAuth + subscription flow.
const response = await fetch(this.callbackUri, {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -281,7 +288,8 @@ class SequoiaSubscribe extends BaseElement {
}
const { recordUri } = data;
- this.state = { type: "subscribed", recordUri, publicationUri };
+ this.subscribed = true;
+ this.state = { type: "idle" };
this.render();
this.dispatchEvent(
@@ -292,7 +300,6 @@ class SequoiaSubscribe extends BaseElement {
}),
);
} catch (error) {
- // Don't overwrite state if we already navigated away
if (this.state.type !== "loading") return;
const message =
@@ -322,21 +329,12 @@ class SequoiaSubscribe extends BaseElement {
}
const isLoading = type === "loading";
- const isSubscribed = type === "subscribed";
const icon = isLoading
? ``
- : isSubscribed
- ? CHECK_ICON
- : BLUESKY_ICON;
-
- const label = isSubscribed ? "Subscribed" : this.label;
- const buttonClass = [
- "sequoia-subscribe-button",
- isSubscribed ? "sequoia-subscribe-button--success" : "",
- ]
- .filter(Boolean)
- .join(" ");
+ : BLUESKY_ICON;
+
+ const label = this.subscribed ? "Unsubscribe on Bluesky" : this.label;
const errorHtml =
type === "error"
@@ -345,11 +343,11 @@ class SequoiaSubscribe extends BaseElement {
this.wrapper.innerHTML = `