Files
Cibello-app/apps/mobile/src/app/paywall.tsx
T
Sven (AAMOS AI) c32a7e33c7
CI / Typecheck, test & build (push) Failing after 2s
ci: trigga på master + formatfix inför Gitea Actions
2026-08-13 17:25:14 +07:00

108 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect } from "react";
import { Alert } from "react-native";
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/api";
import { useAnalytics } from "@/lib/analytics";
import { paywallViewed } from "@app/analytics";
import { t } from "@/lib/i18n";
import { formatMinor } from "@/lib/money";
import {
Body,
Button,
Card,
Heading,
Row,
Screen,
Small,
Spacer,
Tag,
Title,
} from "@/components/ui";
/**
* Paywall (spec §4546). Köpet går via StoreKit/Play Billing i klienten
* (react-native-purchases eller expo-in-app-purchases integreras i fas 7);
* kvittot verifieras ALLTID av backend (spec §61.14). Här visas planerna och
* flödet är förberett med /v1/subscriptions/verify.
*/
interface PlanRow {
key: string;
priceMinor: number;
members: number;
highlight?: boolean;
}
const PLANS: PlanRow[] = [
{ key: "paywall.household", priceMinor: 7900, members: 3 },
{ key: "paywall.family", priceMinor: 12900, members: 6, highlight: true },
{ key: "paywall.large", priceMinor: 16900, members: 12 },
];
interface Entitlements {
plan: string;
status: string;
expiresAt?: string;
}
export default function PaywallScreen() {
const { track } = useAnalytics();
const entitlements = useQuery({
queryKey: ["entitlements"],
queryFn: () => api<Entitlements>("/v1/me/entitlements"),
});
useEffect(() => {
track(
paywallViewed({
properties: { plan: entitlements.data?.plan, status: entitlements.data?.status },
}),
);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const trialDaysLeft =
entitlements.data?.status === "trial" && entitlements.data.expiresAt
? Math.max(0, Math.ceil((Date.parse(entitlements.data.expiresAt) - Date.now()) / 86_400_000))
: null;
const buy = () => {
Alert.alert(t("paywall.soonTitle"), t("paywall.soonBody"));
};
return (
<Screen>
<Title>{t("paywall.title")}</Title>
<Body muted>{t("paywall.subtitle")}</Body>
{trialDaysLeft != null && (
<Tag
label={t("paywall.trialActive", { days: trialDaysLeft, count: trialDaysLeft })}
tone="accent"
/>
)}
<Spacer />
{PLANS.map((plan) => (
<Card
key={plan.key}
style={plan.highlight ? { borderColor: "#16803C", borderWidth: 2 } : undefined}
>
<Row style={{ justifyContent: "space-between" }}>
<Heading>{t(plan.key as never)}</Heading>
{plan.highlight && <Tag label={t("paywall.popular")} tone="success" />}
</Row>
<Body>{t("paywall.perMonth", { price: formatMinor(plan.priceMinor, "SEK") })}</Body>
<Button
label={t("paywall.choose")}
variant={plan.highlight ? "primary" : "secondary"}
onPress={buy}
/>
</Card>
))}
<Small>{t("paywall.fairUse")}</Small>
<Small>{t("paywall.freeNote")}</Small>
<Button label={t("paywall.restore")} variant="ghost" onPress={buy} />
</Screen>
);
}