summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apps/web/app/(marketing)/_components/feature-grid.tsx5
-rw-r--r--apps/web/app/reader/_components/offline-banner.tsx10
-rw-r--r--apps/web/app/reader/_components/reader-layout-shell.tsx2
-rw-r--r--apps/web/app/sw.ts16
-rw-r--r--apps/web/lib/hooks/use-offline-access-sync.ts24
-rw-r--r--packages/shared/source/index.ts3
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