- 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>
156 lines
4.7 KiB
TypeScript
156 lines
4.7 KiB
TypeScript
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);
|