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.
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.
src/SubPage.tsx
// One subscription: what it costs, when it renews, a calendar reminder, its free alternative, and the
// actions: edit, mark as barely used, mark as cancelled, delete.
import { useEffect, useState } from "react";
import { useStore, editSub, deleteSubs } from "./lib/store";
import { CATEGORIES, cycleLabel, nextCharge, onTrial, perMonth } from "./lib/subs";
import { useFreeApps, freeAlternative, STORE_URL } from "./lib/alternatives";
import { remindersIcs, googleLink, firstReminder } from "./lib/ics";
import { money } from "./lib/money";
import { dayKey, fmtDay, relDay } from "./lib/dates";
import { navigate } from "./lib/router";
import { A, Modal, download, toast } from "./ui";
import { Editor } from "./Editor";
import { LeadPicker } from "./Overview";
import { Back, Bell, Download, Leaf, Out, Pencil, Trash } from "./icons";
export function SubPage({ id, edit }: { id: string; edit: boolean }) {
const s = useStore();
const apps = useFreeApps();
const [confirm, setConfirm] = useState(false);
const x = Object.prototype.hasOwnProperty.call(s.subs, id) ? s.subs[id] : undefined;
useEffect(() => { if (x && !x.deleted) document.title = `${x.name} | Subs`; }, [x]);
if (!x || x.deleted) {
return (
<div className="page">
<div className="empty"><p>This subscription is not on this device. It may have been deleted, or added on another device that has not synced yet.</p><A className="btn" href="/all">See all subscriptions</A></div>
</div>
);
}
if (edit) {
return (
<div className="page add">
<A className="back-link" href={`/s/${id}`}><Back size={16} /> {x.name}</A>
<header className="page-head"><div><p className="eyebrow">Edit</p><h1 className="h1">{x.name}</h1></div></header>
<section className="card pad"><Editor initial={x} mode="edit" onDone={() => navigate(`/s/${id}`)} /></section>
</div>
);
}
const today = dayKey();
const trial = onTrial(x, today);
const next = nextCharge(x, today);
const alt = freeAlternative(x.name, apps);
const g = googleLink(x, s.lead, s.currency);
const first = firstReminder(x, s.lead, s.currency);
const host = x.url ? x.url.replace(/^https?:\/\/(www\.)?/, "").replace(/\/.*$/, "") : "";
return (
<div className="page sub">
<A className="back-link" href="/all"><Back size={16} /> All subscriptions</A>
<header className="page-head">
<div>
<p className="eyebrow">{CATEGORIES[x.category]}</p>
<h1 className="h1 sub-title">{x.name}</h1>
<p className="sub-price-big"><strong>{money(x.amount, s.currency)}</strong> {cycleLabel(x)}{trial ? " after the free trial" : ""}</p>
</div>
<A className="btn btn-line" href={`/s/${id}/edit`}><Pencil size={16} /> Edit</A>
</header>
<div className="sub-grid">
<div className="stack col-main">
<dl className="stats">
<div><dt>{x.cancelled ? "Cancelled" : trial ? "Trial ends" : "Next renewal"}</dt><dd>{fmtDay(x.cancelled || (trial ? x.next : next), today, { month: "short", day: "numeric" })}<small> {x.cancelled ? "" : relDay(trial ? x.next : next, today)}</small></dd></div>
<div><dt>Per month</dt><dd>{money(perMonth(x), s.currency)}</dd></div>
<div><dt>Per year</dt><dd>{money(perMonth(x) * 12, s.currency)}</dd></div>
</dl>
{x.cancelled && <p className="notice notice-ok">Marked as cancelled on {fmtDay(x.cancelled, today)}. That saves {money(perMonth(x) * 12, s.currency)} a year. Check that the service confirmed it.</p>}
<section className="card pad stack">
<h2 className="h3">Details</h2>
<dl className="details">
{x.method && <><dt>Paid with</dt><dd>{x.method}</dd></>}
{x.url && <><dt>Manage or cancel</dt><dd><a href={x.url} target="_blank" rel="noopener">{host} <Out size={13} className="inline-icon" /></a></dd></>}
{x.notes && <><dt>Notes</dt><dd className="notes">{x.notes}</dd></>}
{!x.method && !x.url && !x.notes && <dd className="muted">No payment label, website or notes. <A href={`/s/${id}/edit`}>Add them</A></dd>}
</dl>
<p className="hint">Subs does not cancel anything for you and never sees your bank. To cancel, use the service's website or your app store's subscription settings.</p>
</section>
{alt ? (
<section className="card pad stack swaps">
<div className="icon-title"><Leaf /><h2 className="h3">A free alternative</h2></div>
<a className="swap-to" href={alt.app.url} target="_blank" rel="noopener">
<strong>{alt.app.name} by OpenApps <Out size={13} className="inline-icon" /></strong>
<span>{alt.app.tagline} Free instead of {alt.paid}.</span>
</a>
<p className="muted small">If it covers what you use {x.name} for, that is {money(perMonth(x) * 12, s.currency)} a year back. Try it before you cancel.</p>
</section>
) : (
<section className="card pad stack">
<div className="icon-title"><Leaf /><h2 className="h3">No free alternative yet</h2></div>
<p className="muted small">The OpenApps store has no free app for {x.name} yet. <a href={`${STORE_URL}/request`} target="_blank" rel="noopener">Ask for one</a>: the most requested get built next.</p>
</section>
)}
</div>
<div className="stack col-side">
{!x.cancelled && (
<section className="card pad stack">
<div className="icon-title"><Bell /><h2 className="h3">{trial ? "Remind me before the trial ends" : "Remind me before it renews"}</h2></div>
<LeadPicker />
{first && <p className="muted small">First reminder: {first}{trial ? "." : ", then before every renewal."}</p>}
<div className="row">
<button type="button" className="btn btn-line btn-sm" onClick={() => download(`${x.name.replace(/[^\w-]+/g, "-").toLowerCase() || "subscription"}-reminder.ics`, remindersIcs([x], s.lead, s.currency), "text/calendar")}><Download size={16} /> Calendar file (.ics)</button>
{g && <a className="btn btn-line btn-sm" href={g} target="_blank" rel="noopener">Google Calendar <Out size={13} /></a>}
</div>
<p className="hint">Subs cannot send reminders itself, so your calendar does it.</p>
</section>
)}
<section className="card pad stack">
<h2 className="h3">Still worth it?</h2>
<label className="check">
<input type="checkbox" checked={x.unused} disabled={Boolean(x.cancelled)} onChange={(e) => editSub(id, (y) => { y.unused = e.target.checked; })} />
I barely use this
</label>
<p className="muted small">Things you barely use are listed under Review these on the overview, with what they cost you a year.</p>
{x.cancelled ? (
<button type="button" className="btn btn-line btn-sm self-start" onClick={() => { editSub(id, (y) => { y.cancelled = ""; }); toast("Back on your list"); }}>I still pay for it</button>
) : (
<button type="button" className="btn btn-line btn-sm self-start" onClick={() => { editSub(id, (y) => { y.cancelled = today; y.unused = false; }); toast(`Marked ${x.name} as cancelled`, { label: "Undo", run: () => editSub(id, (y) => { y.cancelled = ""; }) }); }}>I cancelled it</button>
)}
<button type="button" className="btn btn-quiet btn-sm btn-danger self-start" onClick={() => setConfirm(true)}><Trash size={16} /> Delete</button>
</section>
</div>
</div>
{confirm && (
<Modal title={`Delete ${x.name}?`} onClose={() => setConfirm(false)} footer={
<>
<button type="button" className="btn btn-danger btn-line" onClick={() => { deleteSubs([id]); toast(`Deleted ${x.name}`); navigate("/all"); }}>Delete</button>
<button type="button" className="btn btn-quiet" onClick={() => setConfirm(false)}>Keep it</button>
</>
}>
<p>This removes it from Subs on every device. To keep it in your history instead, use I cancelled it.</p>
</Modal>
)}
</div>
);
}
Change history
Point store links to openapps.free
Bind agent address in policy
Use OpenApps service URLs and add contribution strip
Bounded source sync
Bounded source sync
Bounded source sync