subs-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev 1ae4feb2bebf
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 5af1a249bbbef1db1b4c5ec8
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
src/lib/store.ts
// The person's subscriptions and settings. They live on the device, so Subs opens instantly and works
// offline, and sync to the person's Bounded account (src/lib/sync.ts).
import { useSyncExternalStore } from "react";
import { localCurrency, rescale } from "./money";
import { live, tombstone, type Sub } from "./subs";

export interface State {
  v: 1;
  owner: string | null; // the account this device copy belongs to
  currency: string;
  lead: number; // days before a renewal that calendar reminders go off
  subs: Record<string, Sub>;
  at: number; // last change to currency or lead, ms
}

export function fresh(): State {
  return { v: 1, owner: null, currency: typeof navigator === "undefined" ? "USD" : localCurrency(), lead: 3, subs: {}, at: 0 };
}

const KEY = "subs:v1";

function load(): State {
  try {
    const raw = localStorage.getItem(KEY);
    if (raw) return { ...fresh(), ...JSON.parse(raw) };
  } catch { /* storage blocked or corrupt: start fresh */ }
  return fresh();
}

let state: State = load();
const listeners = new Set<() => void>();
let afterUpdate: (() => void) | null = null;

export const getState = () => state;
export function onLocalChange(fn: () => void) { afterUpdate = fn; }

export function useStore(): State {
  return useSyncExternalStore((cb) => { listeners.add(cb); return () => listeners.delete(cb); }, () => state);
}

/** Replaces the device copy (after a merge with the account's copy). Does not trigger a sync. */
export function replace(next: State) {
  state = next;
  persist();
}

/** Changes state, saves it on the device and schedules a sync. */
export function update(fn: (s: State) => void) {
  const next: State = structuredClone(state);
  fn(next);
  state = next;
  persist();
  afterUpdate?.();
}

/** Adds or replaces subscriptions, stamped for merging. */
export function saveSubs(subs: Sub[]) {
  const now = Date.now();
  update((s) => { for (const x of subs) s.subs[x.id] = { ...x, at: now }; });
}

export function editSub(id: string, fn: (x: Sub) => void) {
  update((s) => {
    const x = s.subs[id];
    if (!x || x.deleted) return;
    fn(x);
    x.at = Date.now();
  });
}

export function deleteSubs(ids: string[]) {
  update((s) => { for (const id of ids) if (s.subs[id]) s.subs[id] = tombstone(s.subs[id]); });
}

/** Switches currency, keeping every amount as written (9.99 stays 9.99). */
export function setCurrency(currency: string) {
  update((s) => {
    const now = Date.now();
    for (const x of Object.values(s.subs)) {
      const amount = x.deleted ? x.amount : rescale(x.amount, s.currency, currency);
      if (amount !== x.amount) { x.amount = amount; x.at = now; }
    }
    s.currency = currency;
    s.at = now;
  });
}

function persist() {
  try { localStorage.setItem(KEY, JSON.stringify(state)); } catch { /* full or blocked: the account copy still has it */ }
  for (const l of listeners) l();
}

export const listSubs = (s: State) => Object.values(s.subs).filter(live);

Change history

Point store links to openapps.free
1ae4feb2be · 18 hours ago
Bind agent address in policy
99bbab3f0c · 19 hours ago
Use OpenApps service URLs and add contribution strip
781c3c23e5 · 19 hours ago
Bounded source sync
048e3257a5 · 1 day ago
Bounded source sync
7a358a3be6 · 1 day ago
Bounded source sync
0c833fcb0e · 1 day ago