fix(mobile): onboarding pager — measure page width so last slide shows "Start browsing"

scrollTo/onScroll used the full screen width, but the ScrollView viewport is narrower
(card margins + padding), so the computed index desynced from the visible slide — the
last slide kept showing "Next"/"Skip" instead of "Start browsing". Measure the real
viewport width via onLayout and use it for paging, scrollTo and index. Caught on the
emulator (uiautomator dump — FLAG_SECURE blocks screenshots).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jtrzupek 2026-06-22 10:21:19 +02:00
parent db23b63e46
commit 1875604c6d

View file

@ -10,7 +10,6 @@
*/ */
import React from 'react'; import React from 'react';
import { import {
Dimensions,
Modal, Modal,
NativeScrollEvent, NativeScrollEvent,
NativeSyntheticEvent, NativeSyntheticEvent,
@ -97,7 +96,11 @@ export function OnboardingModal() {
const [visible, setVisible] = React.useState(false); const [visible, setVisible] = React.useState(false);
const [index, setIndex] = React.useState(0); const [index, setIndex] = React.useState(0);
const scrollRef = React.useRef<ScrollView>(null); const scrollRef = React.useRef<ScrollView>(null);
const width = Dimensions.get('window').width; // Szerokość STRONY = realna szerokość viewportu ScrollView (mierzona onLayout),
// NIE szerokość ekranu — karta ma marginesy + padding, więc ekran ≠ strona.
// Użycie szerokości ekranu rozjeżdżało paging vs scrollTo vs index (ostatni
// slajd pokazywał "Next" zamiast "Start browsing").
const [pageW, setPageW] = React.useState(0);
// Pierwsze odpalenie: pokaż jeśli jeszcze nie widziany. // Pierwsze odpalenie: pokaż jeśli jeszcze nie widziany.
React.useEffect(() => { React.useEffect(() => {
@ -134,13 +137,14 @@ export function OnboardingModal() {
(i: number) => { (i: number) => {
const clamped = Math.max(0, Math.min(SLIDES.length - 1, i)); const clamped = Math.max(0, Math.min(SLIDES.length - 1, i));
setIndex(clamped); setIndex(clamped);
scrollRef.current?.scrollTo({ x: clamped * width, animated: true }); if (pageW > 0) scrollRef.current?.scrollTo({ x: clamped * pageW, animated: true });
}, },
[width], [pageW],
); );
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => { const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
const i = Math.round(e.nativeEvent.contentOffset.x / width); if (pageW <= 0) return;
const i = Math.round(e.nativeEvent.contentOffset.x / pageW);
if (i !== index) setIndex(i); if (i !== index) setIndex(i);
}; };
@ -164,10 +168,11 @@ export function OnboardingModal() {
pagingEnabled pagingEnabled
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
onMomentumScrollEnd={onScroll} onMomentumScrollEnd={onScroll}
onLayout={(e) => setPageW(e.nativeEvent.layout.width)}
style={styles.pager} style={styles.pager}
> >
{SLIDES.map((s, i) => ( {SLIDES.map((s, i) => (
<View key={i} style={[styles.slide, { width: width - 48 }]}> <View key={i} style={[styles.slide, pageW > 0 && { width: pageW }]}>
<Text style={styles.icon}>{s.icon}</Text> <Text style={styles.icon}>{s.icon}</Text>
<Text style={styles.title}>{s.title}</Text> <Text style={styles.title}>{s.title}</Text>
<View style={styles.lines}> <View style={styles.lines}>