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:
co-authored by
Claude Opus 5.5
parent
5fbd13ba12
commit
f960a119e8
@@ -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);
|
||||
Reference in New Issue
Block a user