Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { QRIcon } from "@entur/icons";
import { useQueries } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { useDevConfig } from "../../context/dev-config";
import { useProfile } from "../../context/profile";
import {
pendingDocumentPollMs,
useCustomerPackages,
} from "../../hooks/use-documents";
import { isPackageNotFound } from "../../lib/omsa-error";
import { getPackages } from "../../lib/ticket-storage";
import { useStoredPackages } from "../../hooks/use-stored-packages";
import {
groupProperties,
groupTravelDocuments,
Expand Down Expand Up @@ -41,19 +38,12 @@ function remainingValidity(end: number, now: number): string {
}

export default function ActiveTicketsSection() {
const { clientFingerprint } = useDevConfig();
const { customer } = useProfile();
const customerId = customer?.id ?? null;
const customerKey = customerId ?? customer?.customerNumber ?? null;
const [stored, setStored] = useState<StoredPackage[]>([]);
const [stored] = useStoredPackages(customerKey);
const customerPackages = useCustomerPackages(customerId);

// Local tickets belong to the active client and customer storage bucket.
// biome-ignore lint/correctness/useExhaustiveDependencies: customerKey changes the storage key used by getPackages()
useEffect(() => {
if (clientFingerprint !== undefined) setStored(getPackages());
}, [clientFingerprint, customerKey]);

const storedById = new Map(stored.map((pkg) => [pkg.packageId, pkg]));
const serverPackages = customerPackages.data?.packages ?? [];
const packages: StoredPackage[] = customerId
Expand All @@ -73,9 +63,7 @@ export default function ActiveTicketsSection() {
...(local?.route ? { route: local.route } : {}),
};
})
: clientFingerprint === undefined
? []
: stored;
: stored;

const serverItemById = new Map(serverPackages.map((item) => [item.id, item]));
const itemQueries = useQueries({
Expand All @@ -84,20 +72,25 @@ export default function ActiveTicketsSection() {
queryFn: () => getPackageItem({ data: pkg.packageId }),
initialData: serverItemById.get(pkg.packageId),
staleTime: 60_000,
retry: (count: number, error: Error) =>
!isPackageNotFound(error) && count < 3,
})),
});
const isConfirmed = (pkg: StoredPackage, i: number) => {
const data = itemQueries[i]?.data;
return (
!!data &&
(data.status ?? data.properties?.status ?? pkg.status) === "CONFIRMED"
);
};
// Only confirmed packages can be active, so skip the documents call for the rest.
const docQueries = useQueries({
queries: packages.map((pkg) => ({
queries: packages.map((pkg, i) => ({
queryKey: ["travel-documents", pkg.packageId],
queryFn: () => getTravelDocuments({ data: pkg.packageId }),
enabled: isConfirmed(pkg, i),
staleTime: 60_000,
refetchInterval: (query: {
state: { data?: TravelDocumentCollection };
}) => pendingDocumentPollMs(query.state.data),
retry: (count: number, error: Error) =>
!isPackageNotFound(error) && count < 3,
})),
});

Expand All @@ -106,10 +99,8 @@ export default function ActiveTicketsSection() {
.flatMap((pkg, i) => {
const item = itemQueries[i];
const documents = docQueries[i];
if (!item?.data || !documents?.data) return [];
if (!item?.data || !documents?.data || !isConfirmed(pkg, i)) return [];
const props = item.data.properties;
if ((item.data.status ?? props?.status ?? pkg.status) !== "CONFIRMED")
return [];
const docs = documents.data.travelDocuments ?? [];
const validDocuments = groupTravelDocuments(docs).flatMap((group) => {
const validity = groupProperties(group);
Expand Down
40 changes: 40 additions & 0 deletions clients/tanstack-start/src/hooks/use-stored-packages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useCallback, useEffect, useState } from "react";
import { useDevConfig } from "../context/dev-config";
import { getPackages } from "../lib/ticket-storage";
import type { StoredPackage } from "../types/documents";

const NONE: StoredPackage[] = [];

/**
* Locally stored packages for the active env, OAuth client and customer.
* Returns an empty list until the active client's fingerprint is known.
*/
export function useStoredPackages(customerKey: string | null) {
const { overrides, clientFingerprint } = useDevConfig();
const scope =
clientFingerprint === undefined
? null
: [
overrides.envMode,
overrides.customerNumber,
clientFingerprint,
customerKey,
].join("|");
const [stored, setStored] = useState<{
scope: string | null;
packages: StoredPackage[];
}>({ scope: null, packages: NONE });

useEffect(() => {
if (scope !== null) setStored({ scope, packages: getPackages() });
}, [scope]);

const reload = useCallback(() => {
if (scope !== null) setStored({ scope, packages: getPackages() });
}, [scope]);

// After an env or client switch, the list read by the effect still belongs
// to the old scope for one render. Querying with it would fetch the old
// env's package IDs against the new env.
return [stored.scope === scope ? stored.packages : NONE, reload] as const;
}
27 changes: 6 additions & 21 deletions clients/tanstack-start/src/routes/tickets/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { ValidTicketIcon } from "@entur/icons";
import { useQueries } from "@tanstack/react-query";
import { createFileRoute, Link } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { useEffect } from "react";
import PageShell from "../../components/layout/PageShell";
import TicketCard from "../../components/tickets/TicketCard";
import { useDevConfig } from "../../context/dev-config";
import { useProfile } from "../../context/profile";
import { useCustomerPackages } from "../../hooks/use-documents";
import { useStoredPackages } from "../../hooks/use-stored-packages";
import { isPackageNotFound } from "../../lib/omsa-error";
import { getPackages, removePackage } from "../../lib/ticket-storage";
import { removePackage } from "../../lib/ticket-storage";
import {
getPackageItem,
getTravelDocuments,
Expand Down Expand Up @@ -51,21 +51,10 @@ function fromServerPackage(
}

function TicketsPage() {
const { clientFingerprint } = useDevConfig();
const { customer } = useProfile();
const customerId = customer?.id ?? null;
const customerKey = customerId ?? customer?.customerNumber ?? null;
const [stored, setStored] = useState<StoredPackage[]>([]);

// Gate the localStorage read until the active client's fingerprint is known,
// so we read from the correct (credential- and customer-scoped) key and
// re-read when the active client or signed-in profile changes. customerKey
// feeds the storage key inside getPackages(), so it must stay a dependency.
// biome-ignore lint/correctness/useExhaustiveDependencies: customerKey scopes getPackages() via storage key
useEffect(() => {
if (clientFingerprint === undefined) return;
setStored(getPackages());
}, [clientFingerprint, customerKey]);
const [stored, reloadStored] = useStoredPackages(customerKey);

// Signed in: the server is the source of truth for which packages are the
// customer's. Anonymous: fall back to the locally stored packageId list.
Expand All @@ -90,8 +79,6 @@ function TicketsPage() {
queryFn: () => getPackageItem({ data: pkg.packageId }),
staleTime: 60_000,
initialData: serverItemById.get(pkg.packageId),
retry: (count: number, error: Error) =>
!isPackageNotFound(error) && count < 3,
})),
});

Expand All @@ -100,8 +87,6 @@ function TicketsPage() {
queryKey: ["travel-documents", pkg.packageId],
queryFn: () => getTravelDocuments({ data: pkg.packageId }),
staleTime: 60_000,
retry: (count: number, error: Error) =>
!isPackageNotFound(error) && count < 3,
})),
});

Expand All @@ -118,8 +103,8 @@ function TicketsPage() {
for (const id of notFoundKey.split(",")) {
removePackage(id);
}
setStored(getPackages());
}, [notFoundKey]);
reloadStored();
}, [notFoundKey, reloadStored]);

const now = new Date();
const active: StoredPackage[] = [];
Expand Down
Loading