Add TPIN creation, TPIN-authorised transfers, MPIN reset and transfer history
- Create TPIN: current MPIN -> OTP -> encrypted TPIN via /tpin/create/*, from Services and from the transfer screen on TPIN_NOT_SET - IMPS initiate now sends encryptedTpin instead of encryptedMpin; handles INVALID_TPIN and TPIN_NOT_SET - Share one RSA-OAEP encryption and key-retry path for MPIN and TPIN - Forgot MPIN on sign-in and Reset MPIN in the Security Center - Fund transfer history list and detail, reachable from Home - Brand logo on sign-in and palette updates Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
abb14d5768
commit
f1def81eb7
+54
-55
@@ -177,54 +177,53 @@ export type Palette = {
|
||||
* ══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
export const light: Palette = {
|
||||
/* Cool neutrals, tinted toward the brand hue (~211°) at 15% saturation so
|
||||
* the greys read as part of the navy family rather than as flat web grey.
|
||||
/* Warm neutrals, tinted toward the brand hue (~2°) at 14% saturation so
|
||||
* the greys read as part of the red family rather than as flat web grey.
|
||||
* Each step keeps the luminance of the navy-tinted ramp it replaced, so
|
||||
* every text-on-surface ratio in this file is unchanged.
|
||||
* Steps are spaced by CIE L*, which is the honest metric between two
|
||||
* near-white fills — a WCAG ratio is not meaningful there. */
|
||||
background: '#EEF2F7', // L* 95.3 app background
|
||||
background: '#F4F0F0', // L* 95.3 app background
|
||||
backgroundElevated: '#FFFFFF',
|
||||
backgroundGradient: ['#FFFFFF', '#F7F9FC', '#EEF2F7'],
|
||||
backgroundGradient: ['#FFFFFF', '#FAF9F8', '#F4F0F0'],
|
||||
surface: '#FFFFFF', // L* 100.0 cards
|
||||
surfaceMuted: '#E2EAF2', // L* 92.3 pressed rows, wells
|
||||
surfaceSunk: '#D5E0EA', // L* 88.6 non-text only
|
||||
border: '#D3DDE7', // 1.32:1 on a card, so it is actually visible
|
||||
borderStrong: '#B4C3D2',
|
||||
surfaceMuted: '#EDE8E7', // L* 92.3 pressed rows, wells
|
||||
surfaceSunk: '#E5DCDC', // L* 88.6 non-text only
|
||||
border: '#E3DAD9', // 1.32:1 on a card, so it is actually visible
|
||||
borderStrong: '#CDBDBD',
|
||||
|
||||
textPrimary: '#0D1B2A', // 17.39 / 15.47 / 14.32
|
||||
textSecondary: '#48596B', // 7.20 / 6.40 / 5.93 — a neutral slate, NOT
|
||||
// the brand navy, which is what it used to be
|
||||
// and why the whole app read as one flat colour
|
||||
textMuted: '#5A697A', // 5.62 / 5.00 / 4.63
|
||||
textOnFill: '#FFFFFF', // white on brand navy is 7.12:1
|
||||
textOnFill: '#FFFFFF', // white on brand red is 5.25:1
|
||||
|
||||
brand: '#124577', // ◀ LOCKED IDENTITY
|
||||
brandLight: '#1B5A96',
|
||||
brandDark: '#0D3459',
|
||||
brandDeep: '#092440',
|
||||
brandSurface: '#E9F0F7',
|
||||
brandBorder: '#B9CDE0',
|
||||
// Stops at brandDark rather than brandDeep: the deepest navy reads as
|
||||
// near-black at the foot of a phone screen, and the wash should still be
|
||||
// recognisably the brand colour where content sits on it.
|
||||
brandGradient: ['#1B5A96', '#124577', '#0D3459'],
|
||||
brand: '#CF2927', // ◀ LOCKED IDENTITY — 5.25 / 4.67 / 4.32. Below
|
||||
// AA on surfaceMuted, so body text on a muted
|
||||
// well should use textPrimary, not brand
|
||||
brandLight: '#E0453F', // decorative only — too light for white text
|
||||
brandDark: '#A81F1D', // pressed fill; white on it 7.29:1
|
||||
brandDeep: '#6E1312',
|
||||
brandSurface: '#FDF0EF', // brand on it 4.72:1; separation from a card 1.11
|
||||
brandBorder: '#F0B9B6', // 1.70:1 against a card
|
||||
// Deliberately DEEPER than `brand`. #CF2927 is too light a red to carry the
|
||||
// translucent on-brand tiers (white @0.75 on it is 3.53:1), so the wash
|
||||
// starts at a darker red, where every tier below clears AA.
|
||||
brandGradient: ['#A81E1C', '#8F1918', '#731413'],
|
||||
|
||||
/* On the navy wash. Each tier is validated against the LIGHTEST stop
|
||||
* (#1B5A96), which is the worst case.
|
||||
/* On the red wash. Each tier is validated against the LIGHTEST stop
|
||||
* (#A81E1C), which is the worst case.
|
||||
*
|
||||
* THE TIERS DO NOT STACK. A translucent fill lightens the wash beneath it,
|
||||
* so a translucent tone on a translucent fill has less to work with:
|
||||
*
|
||||
* white @0.85 on the bare wash 5.64:1 AA
|
||||
* white @0.85 on an @0.12 fill 4.37:1 FAILS
|
||||
* white (solid) on an @0.12 fill 5.36:1 AA
|
||||
*
|
||||
* so a translucent tone on a translucent fill has less to work with.
|
||||
* Text directly on the wash may use any tier. Text on a FILL takes the
|
||||
* solid tone, always. */
|
||||
onBrandPrimary: '#FFFFFF', // 7.12:1 on the lightest stop
|
||||
onBrandPrimary: '#FFFFFF', // 7.32:1 on the lightest stop
|
||||
onBrandSecondary: 'rgba(255, 255, 255, 0.85)', // 5.64:1 — body-safe
|
||||
onBrandMuted: 'rgba(255, 255, 255, 0.75)', // 4.80:1 — still AA
|
||||
onBrandSurface: 'rgba(255, 255, 255, 0.12)', // white on it 5.36:1
|
||||
onBrandSurfaceStrong: 'rgba(255, 255, 255, 0.18)', // white on it 4.66:1 —
|
||||
onBrandMuted: 'rgba(255, 255, 255, 0.75)', // 4.69:1 — still AA
|
||||
onBrandSurface: 'rgba(255, 255, 255, 0.12)', // white on it 5.94:1
|
||||
onBrandSurfaceStrong: 'rgba(255, 255, 255, 0.18)', // white on it 5.22:1 —
|
||||
// this is the ceiling
|
||||
onBrandBorder: 'rgba(255, 255, 255, 0.24)',
|
||||
|
||||
@@ -266,9 +265,9 @@ export const light: Palette = {
|
||||
indigo: { tone: '#4F46E5', surface: '#ECEAFE' }, // 5.85 / 5.07 / 1.13
|
||||
},
|
||||
|
||||
overlay: 'rgba(9, 36, 64, 0.55)',
|
||||
overlay: 'rgba(13, 27, 42, 0.55)',
|
||||
white: '#FFFFFF',
|
||||
shadow: '#092440',
|
||||
shadow: '#0D1B2A',
|
||||
};
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -285,43 +284,43 @@ export const light: Palette = {
|
||||
* borders below — which is why each step is measured against its
|
||||
* neighbour and not in isolation.
|
||||
*
|
||||
* The ground is navy-tinted rather than neutral black: it keeps the app in the
|
||||
* The ground is warm red-tinted rather than neutral black: it keeps the app in the
|
||||
* brand family, and pure #000 under light text is the classic recipe for
|
||||
* halation on an OLED panel.
|
||||
* ══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
export const dark: Palette = {
|
||||
background: '#0B1420',
|
||||
backgroundElevated: '#131F2C',
|
||||
backgroundGradient: ['#131F2C', '#0F1A26', '#0B1420'],
|
||||
surface: '#131F2C',
|
||||
surfaceMuted: '#1B2937',
|
||||
surfaceSunk: '#243343', // non-text; only textPrimary 11.31, textSecondary
|
||||
background: '#171312',
|
||||
backgroundElevated: '#241C1C',
|
||||
backgroundGradient: ['#241C1C', '#1E1817', '#171312'],
|
||||
surface: '#241C1C',
|
||||
surfaceMuted: '#2F2625',
|
||||
surfaceSunk: '#3B2F2E', // non-text; only textPrimary 11.31, textSecondary
|
||||
// 7.39 and brand 4.93 are legible on it
|
||||
border: '#2E4053', // 1.57:1 against a card
|
||||
borderStrong: '#3F556D', // 2.17:1
|
||||
border: '#4A3A3A', // 1.57:1 against a card
|
||||
borderStrong: '#634F4E', // 2.17:1
|
||||
|
||||
textPrimary: '#EAF1F8', // 14.63 / 16.24 / 12.99
|
||||
textSecondary: '#B6C6D6', // 9.55 / 10.60 / 8.48
|
||||
textMuted: '#93A6B9', // 6.66 / 7.40 / 5.92
|
||||
textOnFill: '#0B1420', // ink on `brand` 7.08:1 — white would be 2.61:1
|
||||
textOnFill: '#0B1420', // ink on `brand` 7.44:1 — white would fail
|
||||
|
||||
brand: '#6BA5DC', // 6.38 / 7.08 / 5.67
|
||||
brandLight: '#8CBEE9',
|
||||
brandDark: '#4A80B5',
|
||||
brandDeep: '#0A1E33',
|
||||
brandSurface: '#17304A', // brand on it 5.16:1; separation from a card 1.24
|
||||
brandBorder: '#2D4B6B', // 1.85:1 against a card
|
||||
brand: '#F28580', // 6.70 / 7.44 / 5.95
|
||||
brandLight: '#F7A9A5',
|
||||
brandDark: '#D9564F',
|
||||
brandDeep: '#2A0E0E',
|
||||
brandSurface: '#3A1E24', // brand on it 6.07:1; separation from a card 1.10
|
||||
brandBorder: '#663038', // 1.62:1 against a card
|
||||
// The wash DARKENS in dark mode rather than lightening. White on the
|
||||
// lightest stop is 10.89:1, so every on-brand tier below clears AA with
|
||||
// lightest stop is 11.24:1, so every on-brand tier below clears AA with
|
||||
// room to spare — they were calibrated against the far lighter light-mode
|
||||
// wash, which is the harder case.
|
||||
brandGradient: ['#173F63', '#102D49', '#0A1E33'],
|
||||
brandGradient: ['#6E1E1F', '#521616', '#380F0F'],
|
||||
|
||||
onBrandPrimary: '#FFFFFF', // 10.89:1 on the lightest stop
|
||||
onBrandSecondary: 'rgba(255, 255, 255, 0.85)', // 8.35:1
|
||||
onBrandMuted: 'rgba(255, 255, 255, 0.75)', // 6.72:1
|
||||
onBrandSurface: 'rgba(255, 255, 255, 0.12)', // white on it 7.68:1
|
||||
onBrandPrimary: '#FFFFFF', // 11.24:1 on the lightest stop
|
||||
onBrandSecondary: 'rgba(255, 255, 255, 0.85)', // 8.48:1
|
||||
onBrandMuted: 'rgba(255, 255, 255, 0.75)', // 6.96:1
|
||||
onBrandSurface: 'rgba(255, 255, 255, 0.12)', // white on it 8.24:1
|
||||
onBrandSurfaceStrong: 'rgba(255, 255, 255, 0.18)',
|
||||
onBrandBorder: 'rgba(255, 255, 255, 0.24)',
|
||||
|
||||
|
||||
Reference in New Issue
Block a user