Files
CBS-Mobile-App/src/components/Stepper/index.tsx
T
Abin ToppoandClaude Opus 5.5 f960a119e8 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>
2026-10-14 18:18:40 +05:30

182 lines
5.8 KiB
TypeScript

import React, { memo } from 'react';
import { StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
import { Icon } from '../Icon';
import { useTheme } from '../../theme';
interface StepperProps {
/** Two to four short labels. Longer than four and they stop fitting a 360pt phone. */
steps: readonly string[];
/** Zero-based. Steps before it are done, steps after it are still to come. */
current: number;
/**
* Set when this sits on the navy brand wash rather than a light surface.
*/
onBrand?: boolean;
style?: StyleProp<ViewStyle>;
}
/**
* Where the customer is in a flow, and what is still ahead of them.
*
* Registration is three screens — choose a SIM, enter the code, set an MPIN —
* and until this existed the first of those gave no hint that two more were
* coming. Someone deciding whether to hand over their number is entitled to
* know how long the process is before they start it.
*
* A completed step is marked with a tick as well as a fill, so progress is not
* carried by colour alone. The whole thing is inert: it reports position, it is
* not a way to navigate, and steps are not tappable — you cannot jump to an OTP
* screen before an OTP has been sent.
*/
const StepperComponent = ({ steps, current, onBrand = false, style }: StepperProps) => {
const theme = useTheme();
/**
* The connector, and the outline of a step not yet reached.
*
* On brand this is a white at 0.55 rather than the shared `onBrandBorder`
* (0.24). A border token is measured for a panel edge, which sits next to a
* fill; a hairline on the open wash has nothing beside it and 0.24 came to
* 1.75:1 — under the 3:1 that non-text contrast asks for. At 0.55 it is
* 3.34:1 against the lightest point of the gradient and, at one point tall,
* still reads as a rule rather than a bar.
*
* On a light surface it stays a border tone. The rail is reinforcement in
* either case: sequence is carried by the numbers, and completion by the fill
* and the tick, so a reader who cannot make out the line loses nothing.
*/
const railColour = onBrand ? 'rgba(255, 255, 255, 0.55)' : theme.colors.cardBorderStrong;
const doneColour = onBrand ? theme.colors.onBrandPrimary : theme.colors.primary;
return (
<View
accessibilityRole="progressbar"
accessibilityLabel={`Step ${current + 1} of ${steps.length}: ${steps[current] ?? ''}`}
style={[styles.container, style]}
>
{steps.map((label, index) => {
const done = index < current;
const active = index === current;
/**
* A done or active marker is a solid fill, so the number and tick on it
* are the solid tone — the translucent on-brand tiers are measured
* against the bare wash and lose their contrast over a fill.
*/
const markerFill = done || active ? doneColour : 'transparent';
const markerBorder = done || active ? doneColour : railColour;
const markerInk = done || active
? onBrand
? theme.colors.primary
: theme.colors.textOnBrand
: onBrand
? theme.colors.onBrandSecondary
: theme.colors.textMuted;
const labelColour = active
? onBrand
? theme.colors.onBrandPrimary
: theme.colors.textPrimary
: onBrand
? theme.colors.onBrandSecondary
: theme.colors.textMuted;
return (
<View key={label} style={styles.step}>
{/* The rails are halves either side of the marker, each flexing to
fill its own gap. Drawing one line behind the whole row instead
would need an absolute overlay measured against the markers,
which drifts as soon as a label wraps. */}
<View style={styles.railRow}>
<View
style={[
styles.rail,
{ backgroundColor: index === 0 ? 'transparent' : railColour },
]}
/>
<View
style={[
styles.marker,
{ backgroundColor: markerFill, borderColor: markerBorder },
]}
>
{done ? (
<Icon name="checkmark" size={13} color={markerInk} />
) : (
<Text style={[theme.text.eyebrow, styles.markerText, { color: markerInk }]}>
{index + 1}
</Text>
)}
</View>
<View
style={[
styles.rail,
{
backgroundColor: index === steps.length - 1 ? 'transparent' : railColour,
},
]}
/>
</View>
<Text
numberOfLines={2}
style={[
active ? theme.text.bodyStrong : theme.text.bodySmall,
styles.label,
{ color: labelColour },
]}
>
{label}
</Text>
</View>
);
})}
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'flex-start',
},
step: {
flex: 1,
alignItems: 'center',
gap: 7,
},
railRow: {
flexDirection: 'row',
alignItems: 'center',
alignSelf: 'stretch',
},
rail: {
flex: 1,
height: 1,
},
marker: {
width: 24,
height: 24,
borderRadius: 12,
borderWidth: 1.5,
alignItems: 'center',
justifyContent: 'center',
},
markerText: {
fontSize: 11,
lineHeight: 14,
letterSpacing: 0,
},
label: {
fontSize: 12,
lineHeight: 16,
textAlign: 'center',
paddingHorizontal: 2,
},
});
export const Stepper = memo(StepperComponent);