Add Positive Pay, statement guide v1.0, onboarding, and switch every feature on

- Positive Pay on the 12 Oct /pps contract (account number, not account ID):
  OTP enrollment, cheque submit, saved cheques, detail and stop payment.
  Full flow passed on the phone against mobile-banking-backend.lincpay.
- Statement & Mini Statement guide v1.0 (14 Oct): mini statement JSON on
  Home and the Statement screen; dated PDF export with the password rule.
- Onboarding slides and splash screen, Inter font, shared Icon and
  TypewriterText components, nearby branches via OpenStreetMap.
- Every feature switched on; the 14 Oct switch set is kept in
  featureConfig.stage-2026-10-14.ts. Loans run on demo data until the
  backend has /loans routes.
- npm run android wraps run-android for shells spawned from VS Code.
- OpenProject work packages updated to 14-10-2026 status, with screenshots
  of Positive Pay, statement, loans and cards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Abin Toppo
2026-10-14 18:18:40 +05:30
co-authored by Claude Opus 5.5
parent 5fbd13ba12
commit f960a119e8
510 changed files with 20268 additions and 6483 deletions
+155
View File
@@ -0,0 +1,155 @@
import React, { memo, useMemo } from 'react';
import { StatusBar, StyleSheet, Text, useWindowDimensions, View } from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import Svg, { G, Path, Rect } from 'react-native-svg';
import Animated, { FadeIn, FadeInUp } from 'react-native-reanimated';
import { useTheme } from '../../../theme';
import { BrandLogo } from '../components/BrandLogo';
/**
* The skyline, on a 400 × 220 canvas anchored to the bottom edge.
*
* Two layers of towers: a far row drawn in a translucent white, so it reads as
* haze against whatever navy the theme supplies, and a near row in the wash's
* darkest stop, with lit windows. Fixed geometry rather than random, so the
* splash is the same picture on every launch.
*/
const FAR: ReadonlyArray<[x: number, w: number, h: number]> = [
[0, 34, 92], [30, 26, 120], [52, 40, 84], [88, 22, 140], [106, 36, 104],
[140, 30, 156], [196, 24, 128], [224, 42, 96], [262, 28, 150], [286, 38, 110],
[320, 24, 132], [340, 36, 88], [372, 28, 118],
];
const NEAR: ReadonlyArray<[x: number, w: number, h: number]> = [
[-4, 40, 64], [32, 30, 96], [60, 46, 72], [104, 28, 112], [150, 32, 128],
[210, 30, 104], [238, 44, 70], [280, 32, 118], [310, 40, 80], [348, 30, 100],
[376, 30, 66],
];
const CANVAS_W = 400;
const CANVAS_H = 220;
/** Lit windows for one near tower — a deterministic sprinkle, not a grid. */
const windowsFor = (x: number, w: number, h: number, seed: number) => {
const lit: Array<{ x: number; y: number }> = [];
const columns = Math.max(1, Math.floor((w - 8) / 8));
for (let row = 0; row * 10 + 14 < h - 6; row += 1) {
for (let col = 0; col < columns; col += 1) {
if ((row * 5 + col * 3 + seed) % 4 === 0) {
lit.push({ x: x + 6 + col * 8, y: CANVAS_H - h + 10 + row * 10 });
}
}
}
return lit;
};
const Skyline = memo(({ width }: { width: number }) => {
const theme = useTheme();
const nearColor = theme.colors.brandGradient[2];
const windows = useMemo(
() => NEAR.flatMap(([x, w, h], i) => windowsFor(x, w, h, i)),
[],
);
return (
<Svg
width={width}
height={(width * CANVAS_H) / CANVAS_W}
viewBox={`0 0 ${CANVAS_W} ${CANVAS_H}`}
preserveAspectRatio="xMidYMax slice"
>
{/* far row */}
<G fill={theme.colors.onBrandSurface}>
{FAR.map(([x, w, h]) => (
<Rect key={`f${x}`} x={x} y={CANVAS_H - h} width={w} height={h} />
))}
{/* the landmark tower, with its spire */}
<Rect x={170} y={CANVAS_H - 188} width={26} height={188} />
<Path d={`M176 ${CANVAS_H - 188} L183 ${CANVAS_H - 214} L190 ${CANVAS_H - 188} Z`} />
</G>
{/* near row */}
<G fill={nearColor}>
{NEAR.map(([x, w, h]) => (
<Rect key={`n${x}`} x={x} y={CANVAS_H - h} width={w} height={h} />
))}
</G>
{/* windows */}
<G fill={theme.colors.accentFill} opacity={0.75}>
{windows.map((pane) => (
<Rect key={`${pane.x}-${pane.y}`} x={pane.x} y={pane.y} width={3} height={4} />
))}
</G>
</Svg>
);
});
/**
* The launch screen, shown while the app restores its session and resolves
* its feature flags.
*
* The navy wash with the bank's logo on its white plate, a one-line promise,
* and the skyline along the foot. Light status-bar icons in both themes, since
* the wash is dark in both.
*/
const SplashScreenComponent = () => {
const theme = useTheme();
const { width } = useWindowDimensions();
return (
<View style={styles.flex}>
<StatusBar barStyle="light-content" backgroundColor="transparent" translucent />
<LinearGradient
colors={theme.colors.brandGradient}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 1 }}
style={StyleSheet.absoluteFill}
/>
<View style={styles.skyline} pointerEvents="none">
<Skyline width={width} />
</View>
<View style={styles.centre}>
<Animated.View entering={FadeIn.duration(500)}>
<BrandLogo width={210} />
</Animated.View>
<Animated.View entering={FadeInUp.delay(250).duration(500)}>
<Text style={[theme.text.body, styles.tagline, { color: theme.colors.onBrandSecondary }]}>
Banking for a brighter tomorrow
</Text>
</Animated.View>
</View>
</View>
);
};
const styles = StyleSheet.create({
flex: {
flex: 1,
},
centre: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 18,
// Sits the lock-up a little above centre, clear of the skyline.
paddingBottom: '30%',
},
tagline: {
textAlign: 'center',
},
skyline: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
},
});
export const SplashScreen = memo(SplashScreenComponent);