Förberedelse fas 2: testmiljö fix + analytics tracker i mobilappen
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { router } from "expo-router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api } from "@/lib/api";
|
||||
import { useAnalytics } from "@/lib/analytics";
|
||||
import { recommendationsViewed } from "@app/analytics";
|
||||
import { t } from "@/lib/i18n";
|
||||
import {
|
||||
Body,
|
||||
@@ -49,6 +51,7 @@ interface WhatToEatResponse {
|
||||
export default function WhatToEatScreen() {
|
||||
const [craving, setCraving] = useState("");
|
||||
const [submittedCraving, setSubmittedCraving] = useState("");
|
||||
const { track } = useAnalytics();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["what-to-eat", submittedCraving],
|
||||
@@ -58,6 +61,20 @@ export default function WhatToEatScreen() {
|
||||
),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (query.data) {
|
||||
track(
|
||||
recommendationsViewed({
|
||||
properties: {
|
||||
craving: submittedCraving || undefined,
|
||||
count: query.data.recommendations.length,
|
||||
hasMealBoxSuggestions: query.data.mealBoxSuggestions.length > 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [query.data]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<Title>{t("wte.title")}</Title>
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
Title,
|
||||
} from "@/components/ui";
|
||||
import { spacing } from "@/lib/theme";
|
||||
import { useAnalytics } from "@/lib/analytics";
|
||||
import { scanStarted } from "@app/analytics";
|
||||
import { FirstScanCoach, useFirstScanCoach } from "@/components/FirstScanCoach";
|
||||
|
||||
/** Skanna (spec §4.2): kyl, frys, skafferi, ingredienser, tallrik, kvitto, streckkod, datum, näringsdeklaration. */
|
||||
@@ -45,6 +47,7 @@ interface Entitlements {
|
||||
export default function ScanScreen() {
|
||||
const [busyType, setBusyType] = useState<string | null>(null);
|
||||
const coach = useFirstScanCoach();
|
||||
const { track } = useAnalytics();
|
||||
const entitlements = useQuery({
|
||||
queryKey: ["entitlements"],
|
||||
queryFn: () => api<Entitlements>("/v1/me/entitlements"),
|
||||
@@ -55,6 +58,7 @@ export default function ScanScreen() {
|
||||
}, []);
|
||||
|
||||
const startScan = async (scanType: string) => {
|
||||
track(scanStarted({ properties: { scanType } }));
|
||||
if (scanType === "barcode") {
|
||||
router.push("/barcode");
|
||||
return;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { QueryClient } from "@tanstack/react-query";
|
||||
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
|
||||
import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { AnalyticsProvider } from "@/lib/analytics";
|
||||
import { BRAND } from "@/lib/brand";
|
||||
import { t, useI18nVersion } from "@/lib/i18n";
|
||||
import { colors } from "@/lib/theme";
|
||||
@@ -51,7 +52,8 @@ export default function RootLayout() {
|
||||
client={queryClient}
|
||||
persistOptions={{ persister }}
|
||||
>
|
||||
<StatusBar style="dark" />
|
||||
<AnalyticsProvider>
|
||||
<StatusBar style="dark" />
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerStyle: { backgroundColor: colors.background },
|
||||
@@ -87,7 +89,8 @@ export default function RootLayout() {
|
||||
options={{ title: t("myday.logMeal"), presentation: "modal" }}
|
||||
/>
|
||||
<Stack.Screen name="barcode" options={{ title: "Streckkod" }} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</AnalyticsProvider>
|
||||
</PersistQueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { router } from "expo-router";
|
||||
import { api } from "@/lib/api";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { useAnalytics } from "@/lib/analytics";
|
||||
import { t } from "@/lib/i18n";
|
||||
import {
|
||||
onboardingStarted,
|
||||
onboardingStepCompleted,
|
||||
onboardingSkipped,
|
||||
} from "@app/analytics";
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
@@ -59,8 +65,13 @@ export default function OnboardingScreen() {
|
||||
const setOnboardingCompleted = useAuth((s) => s.setOnboardingCompleted);
|
||||
const setOnboardingStep = useAuth((s) => s.setOnboardingStep);
|
||||
const savedStep = useAuth((s) => s.onboardingStep);
|
||||
const { track } = useAnalytics();
|
||||
const [layer, setLayer] = useState<OnboardingLayer>(savedStep ?? "a");
|
||||
|
||||
useEffect(() => {
|
||||
track(onboardingStarted({ properties: { step: layer } }));
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Step A state
|
||||
const [goal, setGoal] = useState<string | null>(null);
|
||||
const [mode, setMode] = useState<"simple" | "exact">("simple");
|
||||
@@ -94,6 +105,7 @@ export default function OnboardingScreen() {
|
||||
precisionMode: mode,
|
||||
},
|
||||
});
|
||||
track(onboardingStepCompleted({ properties: { step: "a", goal, precisionMode: mode } }));
|
||||
setOnboardingStep(res.step);
|
||||
setLayer("b");
|
||||
// Let user into the app – Step B will be shown contextually later
|
||||
@@ -125,6 +137,7 @@ export default function OnboardingScreen() {
|
||||
: { kind: "skip" },
|
||||
},
|
||||
});
|
||||
track(onboardingStepCompleted({ properties: { step: "b", diet, allergens, householdKind } }));
|
||||
setOnboardingStep(res.step);
|
||||
setLayer("c");
|
||||
} catch (err) {
|
||||
@@ -138,6 +151,7 @@ export default function OnboardingScreen() {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
track(onboardingStepCompleted({ properties: { step: "c" } }));
|
||||
await api("/v1/onboarding/complete-c", {
|
||||
method: "POST",
|
||||
body: {
|
||||
@@ -162,6 +176,7 @@ export default function OnboardingScreen() {
|
||||
const skip = async (targetLayer?: OnboardingLayer) => {
|
||||
setBusy(true);
|
||||
try {
|
||||
track(onboardingSkipped({ properties: { step: targetLayer ?? layer } }));
|
||||
await api("/v1/onboarding/skip", {
|
||||
method: "POST",
|
||||
body: { step: targetLayer },
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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 {
|
||||
@@ -43,11 +46,16 @@ interface Entitlements {
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Alert, View } from "react-native";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
Tag,
|
||||
} from "@/components/ui";
|
||||
import { spacing } from "@/lib/theme";
|
||||
import { useAnalytics } from "@/lib/analytics";
|
||||
import { scanReviewOpened } from "@app/analytics";
|
||||
import { parseUnitInput, unitLabel } from "@/lib/units";
|
||||
|
||||
/**
|
||||
@@ -70,6 +72,11 @@ export default function ScanReviewScreen() {
|
||||
const queryClient = useQueryClient();
|
||||
const [items, setItems] = useState<EditableItem[] | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const { track } = useAnalytics();
|
||||
|
||||
useEffect(() => {
|
||||
if (jobId) track(scanReviewOpened({ properties: { jobId } }));
|
||||
}, [jobId]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["scan", jobId],
|
||||
|
||||
Reference in New Issue
Block a user