diff options
| author | Fuwn <[email protected]> | 2026-02-10 01:32:16 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-10 01:32:16 -0800 |
| commit | 293f9ccc308b69f507e752af01a49652faee330f (patch) | |
| tree | b699e52309cfab03d539a048de34a98158d6168b | |
| parent | feat: add automatic timeline refresh with scroll position preservation (diff) | |
| download | asa.news-293f9ccc308b69f507e752af01a49652faee330f.tar.xz asa.news-293f9ccc308b69f507e752af01a49652faee330f.zip | |
feat: gate offline reading to pro and developer plans
Service worker now only caches Supabase REST responses when the user's
tier allows offline reading. Client syncs tier status to SW via
postMessage after profile loads. Free users see a descriptive offline
banner instead of stale cached data.
| -rw-r--r-- | apps/web/app/(marketing)/_components/feature-grid.tsx | 5 | ||||
| -rw-r--r-- | apps/web/app/reader/_components/offline-banner.tsx | 10 | ||||
| -rw-r--r-- | apps/web/app/reader/_components/reader-layout-shell.tsx | 2 | ||||
| -rw-r--r-- | apps/web/app/sw.ts | 16 | ||||
| -rw-r--r-- | apps/web/lib/hooks/use-offline-access-sync.ts | 24 | ||||
| -rw-r--r-- | packages/shared/source/index.ts | 3 |
6 files changed, 59 insertions, 1 deletions
diff --git a/apps/web/app/(marketing)/_components/feature-grid.tsx b/apps/web/app/(marketing)/_components/feature-grid.tsx index 640b5d0..9f3fdcc 100644 --- a/apps/web/app/(marketing)/_components/feature-grid.tsx +++ b/apps/web/app/(marketing)/_components/feature-grid.tsx @@ -29,6 +29,11 @@ const FEATURES = [ description: "new entries appear automatically or via notification \u2014 your choice. scroll position is always preserved.", }, + { + title: "offline reading", + description: + "read cached articles without an internet connection. available on pro and developer plans.", + }, ] export function FeatureGrid() { diff --git a/apps/web/app/reader/_components/offline-banner.tsx b/apps/web/app/reader/_components/offline-banner.tsx index 81ebf8f..b6089de 100644 --- a/apps/web/app/reader/_components/offline-banner.tsx +++ b/apps/web/app/reader/_components/offline-banner.tsx @@ -1,6 +1,8 @@ "use client" import { useSyncExternalStore } from "react" +import { TIER_LIMITS, type SubscriptionTier } from "@asa-news/shared" +import { useUserProfile } from "@/lib/queries/use-user-profile" function subscribe(callback: () => void) { window.addEventListener("online", callback) @@ -21,12 +23,18 @@ function getServerSnapshot() { export function OfflineBanner() { const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) + const { data: userProfile } = useUserProfile() if (isOnline) return null + const tier = (userProfile?.tier ?? "free") as SubscriptionTier + const allowsOffline = TIER_LIMITS[tier]?.allowsOfflineReading ?? false + return ( <div className="border-b border-border bg-background-tertiary px-3 py-1.5 text-center text-text-dim"> - you are offline — showing cached content + {allowsOffline + ? "you are offline \u2014 showing cached content" + : "you are offline \u2014 offline reading is available on pro and developer plans"} </div> ) } diff --git a/apps/web/app/reader/_components/reader-layout-shell.tsx b/apps/web/app/reader/_components/reader-layout-shell.tsx index 7ec1002..17ae0b5 100644 --- a/apps/web/app/reader/_components/reader-layout-shell.tsx +++ b/apps/web/app/reader/_components/reader-layout-shell.tsx @@ -14,6 +14,7 @@ import { KeyboardShortcutsDialog } from "./keyboard-shortcuts-dialog" import { MfaChallenge } from "./mfa-challenge" import { OfflineBanner } from "./offline-banner" import { useKeyboardNavigation } from "@/lib/hooks/use-keyboard-navigation" +import { useOfflineAccessSync } from "@/lib/hooks/use-offline-access-sync" import { createSupabaseBrowserClient } from "@/lib/supabase/client" import { useSubscriptions } from "@/lib/queries/use-subscriptions" import { useCustomFeeds } from "@/lib/queries/use-custom-feeds" @@ -58,6 +59,7 @@ export function ReaderLayoutShell({ const { data: subscriptionsData } = useSubscriptions() const { data: customFeedsData } = useCustomFeeds() const { data: userProfile } = useUserProfile() + useOfflineAccessSync(userProfile?.tier as "free" | "pro" | "developer" | undefined) const sidebarMaxWidth = useMemo(() => { if (typeof window === "undefined") return "35%" diff --git a/apps/web/app/sw.ts b/apps/web/app/sw.ts index c6dd082..955856b 100644 --- a/apps/web/app/sw.ts +++ b/apps/web/app/sw.ts @@ -11,6 +11,21 @@ declare global { declare const self: ServiceWorkerGlobalScope +let offlineReadingAllowed = false + +self.addEventListener("message", (event) => { + if (event.data?.type === "SET_OFFLINE_ACCESS") { + offlineReadingAllowed = event.data.allowed === true + } +}) + +const offlineGatePlugin = { + cacheWillUpdate: async ({ response }: { response: Response }): Promise<Response | null> => { + if (!offlineReadingAllowed) return null + return response + }, +} + const sameOriginCache = defaultCache.map((entry) => ({ ...entry, matcher: (parameters: Parameters<Exclude<(typeof defaultCache)[number]["matcher"], RegExp | string | undefined>>[0]) => { @@ -37,6 +52,7 @@ const serwist = new Serwist({ cacheName: "supabase-rest-get", networkTimeoutSeconds: 10, plugins: [ + offlineGatePlugin, new ExpirationPlugin({ maxEntries: 200, maxAgeSeconds: 60 * 60 * 24, diff --git a/apps/web/lib/hooks/use-offline-access-sync.ts b/apps/web/lib/hooks/use-offline-access-sync.ts new file mode 100644 index 0000000..a324d86 --- /dev/null +++ b/apps/web/lib/hooks/use-offline-access-sync.ts @@ -0,0 +1,24 @@ +"use client" + +import { useEffect } from "react" +import { TIER_LIMITS, type SubscriptionTier } from "@asa-news/shared" + +export function useOfflineAccessSync(tier: SubscriptionTier | undefined) { + useEffect(() => { + if (!tier) return + + const allowed = TIER_LIMITS[tier]?.allowsOfflineReading ?? false + + navigator.serviceWorker?.controller?.postMessage({ + type: "SET_OFFLINE_ACCESS", + allowed, + }) + + navigator.serviceWorker?.ready.then((registration) => { + registration.active?.postMessage({ + type: "SET_OFFLINE_ACCESS", + allowed, + }) + }) + }, [tier]) +} diff --git a/packages/shared/source/index.ts b/packages/shared/source/index.ts index cf3a124..335a74e 100644 --- a/packages/shared/source/index.ts +++ b/packages/shared/source/index.ts @@ -13,6 +13,7 @@ export const TIER_LIMITS = { allowsManualRefresh: false, allowsApiAccess: false, allowsWebhooks: false, + allowsOfflineReading: false, }, pro: { maximumFeeds: 200, @@ -26,6 +27,7 @@ export const TIER_LIMITS = { allowsManualRefresh: true, allowsApiAccess: false, allowsWebhooks: false, + allowsOfflineReading: true, }, developer: { maximumFeeds: 500, @@ -39,6 +41,7 @@ export const TIER_LIMITS = { allowsManualRefresh: true, allowsApiAccess: true, allowsWebhooks: true, + allowsOfflineReading: true, }, } as const |