108 lines
3.0 KiB
TypeScript
108 lines
3.0 KiB
TypeScript
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 §45–46). 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>
|
||
);
|
||
}
|