Files
TREK/client/native-shell/amount.ts
T
Maurice 52f0fb882f feat(native): redesign the app's first screens around a living landscape
The screens the app ships itself (welcome, plans, address, own server and
the states between) move from the dark card layout to a light design on an
animated aerial landscape drawn by a WebGL shader (native-shell/scenery.ts):
an island, a coastal town, farmland and mountains in one world.

- Welcome: the landscape with the TREK wordmark, three perks in one card,
  "Explore cloud plans" and "I already have a TREK" as two buttons.
- Plans: one tab per plan; choosing one flies the camera to its place,
  further inland with every plan. Each plan reads as a kind of trip, the
  price sits in a card with a monthly/yearly switch and counts to its new
  amount in the store's own format (native-shell/amount.ts), and a yearly
  price shows what it makes a month. Restore purchases, the renewal terms
  and the links stay as the store rules require.
- Address: the address writes itself on a glass card over the landscape;
  free, the password rules and the agreement sit on white cards.
- Own server: the server address on the glass card; a failed check turns
  the card red with what went wrong, proxy sign-in and client certificate
  panels in the same style.
- States: server not reachable, certificate needed, already subscribed,
  setting up (with its steps, then "Arrived") and the slow setup.
- iPad, an unfolded phone and a phone on its side get two columns, the
  landscape beside the content; on a dual-screen device each half takes
  one screen. Arabic and Hebrew mirror through logical sides only.
- The shader caps the pixel ratio at 2, drops to a lower resolution on a
  slow device, stops in the background and is one still frame with
  reduced motion.
- The sign-in and error page bars over a proxy's page (iOS and Android)
  take the same look: a white card and a dark button.
- Plus Jakarta Sans is bundled for the text, with Montserrat standing in
  for Cyrillic; the wordmark fonts and the old preview images are gone.
- New strings in all 28 languages; the keys of the old screens removed.
- The dev sample plans follow the cloud's plans (start, basis, plus, pro).
2026-11-12 14:18:42 +01:00

44 lines
1.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
/**
* The amount inside a price as the store writes it ("4,90 €", "$4.99",
* "CHF 12.90", "1.290 ₽"), and a way to write another amount the same way: the
* same currency, the same decimal mark, the same digit grouping. The plan picker
* counts a price up to its new value with it, and turns a yearly price into what
* it makes a month, without knowing the store's locale.
*/
interface Amount {
/** The amount as a number, in the price's own unit (4.9 for "4,90 €"). */
value: number;
/** The price written for another amount, in the same format. */
render(value: number): string;
}
// Digits with the marks stores put between them: . , space, no-break spaces, and
// the apostrophes Swiss formats group thousands with (1'290 and 1’290).
const NUMBER = /\d[\d.,\s  '’]*\d|\d/;
export function amountIn(label: string): Amount | null {
const match = NUMBER.exec(label);
if (!match) return null;
const raw = match[0];
// The decimal mark is the last . or , followed by one or two digits to the end.
const decimal = /([.,])(\d{1,2})$/.exec(raw);
const decimals = decimal ? decimal[2].length : 0;
const mark = decimal ? decimal[1] : '';
const whole = decimal ? raw.slice(0, raw.length - decimal[0].length) : raw;
const group = /[.,\s  '’]/.exec(whole)?.[0] ?? '';
const digits = whole.replace(/\D/g, '') + (decimal ? `.${decimal[2]}` : '');
const value = Number(digits);
if (!Number.isFinite(value)) return null;
const before = label.slice(0, match.index);
const after = label.slice(match.index + raw.length);
return {
value,
render(next: number) {
const fixed = Math.max(0, next).toFixed(decimals);
const [int, frac] = fixed.split('.');
const grouped = group ? int.replace(/\B(?=(\d{3})+(?!\d))/g, group) : int;
return `${before}${grouped}${frac !== undefined ? mark + frac : ''}${after}`;
},
};
}