291 lines
10 KiB
TypeScript
291 lines
10 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react';
|
||
import { View, Text, StyleSheet, TouchableOpacity, Platform, Alert, Image, useWindowDimensions } from 'react-native';
|
||
import { useRouter } from 'expo-router';
|
||
import * as WebBrowser from 'expo-web-browser';
|
||
import { Trans, useTranslation } from 'react-i18next';
|
||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||
import { setConsentAccepted, getConsentAccepted } from '../../src/storage/appStorage';
|
||
import { fetchLegalLinks } from '@/src/services/legalApi';
|
||
import { getOnboardingCompleted } from '@/src/storage/appStorage';
|
||
import { API_BASE_URL } from '@/src/constants/env';
|
||
import { isTraditionalChineseLocaleTag } from '@/src/i18n/locale';
|
||
|
||
// 导入 SVG 组件
|
||
import FlowersBg from '../../assets/images/index/flowers_endbg.svg';
|
||
import WelcomeBtn from '../../assets/images/index/welcome_btn.svg';
|
||
|
||
// 繁中開屏 consent 文案:寫死在元件內,避免 Metro/iOS bundle 快取導致永遠顯示舊文案。
|
||
// 若需修改,請改這裡並同步 client/src/i18n/locales/zh-TW.json 的 consent 區塊。
|
||
const ZH_TW_CONSENT = {
|
||
title: '我們知道,',
|
||
subtitle: '當媽媽很不容易。',
|
||
subtitleSecondary: '這裡給你一些溫柔的肯定與提醒',
|
||
};
|
||
|
||
export default function SplashScreen() {
|
||
const router = useRouter();
|
||
const { t, i18n } = useTranslation();
|
||
const { width, height } = useWindowDimensions();
|
||
const [showConsent, setShowConsent] = useState(false);
|
||
const isTablet = Platform.OS === 'ios' && Math.min(width, height) >= 768;
|
||
|
||
// 繁中時強制使用上方常數(含 zh-TW / zh-Hant / zh-Hant-TW),其餘用 i18n
|
||
const isZhTW = isTraditionalChineseLocaleTag(i18n.language || '');
|
||
const title = isZhTW ? ZH_TW_CONSENT.title : t('consent.title');
|
||
const subtitle = isZhTW ? ZH_TW_CONSENT.subtitle : t('consent.subtitle');
|
||
const subtitleSecondary = isZhTW ? ZH_TW_CONSENT.subtitleSecondary : t('consent.subtitleSecondary');
|
||
|
||
useEffect(() => {
|
||
if (typeof __DEV__ !== 'undefined' && __DEV__ && showConsent) {
|
||
console.log('[i18n consent] language=', i18n.language, 'title=', title, 'subtitle=', subtitle);
|
||
}
|
||
}, [showConsent, i18n.language, title, subtitle]);
|
||
const [links, setLinks] = useState<{ privacy?: string; terms?: string }>({});
|
||
const [linksLoading, setLinksLoading] = useState(false);
|
||
const mountedRef = useRef(true);
|
||
|
||
useEffect(() => {
|
||
checkConsent();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
return () => {
|
||
mountedRef.current = false;
|
||
};
|
||
}, []);
|
||
|
||
const checkConsent = async () => {
|
||
const accepted = await getConsentAccepted();
|
||
setShowConsent(!accepted);
|
||
if (accepted) {
|
||
// 已同意协议则直接分发到目标页,避免先回到 /(index)再二次跳转导致“闪一下”
|
||
const completed = await getOnboardingCompleted();
|
||
if (completed) {
|
||
router.replace('/(app)/home');
|
||
} else {
|
||
router.replace('/(onboarding)/onboarding');
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleAgree = async () => {
|
||
await setConsentAccepted(true);
|
||
setShowConsent(false);
|
||
// 跳转到 onboarding 流程
|
||
router.replace('/(onboarding)/onboarding');
|
||
};
|
||
|
||
const openLink = async (url: string) => {
|
||
try {
|
||
await WebBrowser.openBrowserAsync(url);
|
||
} catch (error) {
|
||
Alert.alert(t('common.error'), t('common.openLinkError'));
|
||
}
|
||
};
|
||
|
||
async function refreshLegalLinks(): Promise<{ privacy?: string; terms?: string }> {
|
||
if (mountedRef.current) setLinksLoading(true);
|
||
try {
|
||
const res = await fetchLegalLinks();
|
||
const next = { privacy: res.privacyPolicyUrl, terms: res.termsOfUseUrl };
|
||
if (mountedRef.current) setLinks(next);
|
||
return next;
|
||
} catch (e) {
|
||
// 不阻塞主流程:失败时不崩溃,链接入口仍可点(会提示)
|
||
if (__DEV__) console.log('[LegalLinks] 拉取失败(splash):', e);
|
||
if (mountedRef.current) setLinks({});
|
||
return {};
|
||
} finally {
|
||
if (mountedRef.current) setLinksLoading(false);
|
||
}
|
||
}
|
||
|
||
async function handleOpenLegal(type: 'privacy' | 'terms') {
|
||
const currentUrl = type === 'privacy' ? links.privacy : links.terms;
|
||
if (currentUrl) {
|
||
await openLink(currentUrl);
|
||
return;
|
||
}
|
||
|
||
// 链接还没拿到/拉取失败:点击时主动再拉一次,避免“点了没反应”
|
||
const next = await refreshLegalLinks();
|
||
const nextUrl = type === 'privacy' ? next.privacy : next.terms;
|
||
if (nextUrl) {
|
||
await openLink(nextUrl);
|
||
return;
|
||
}
|
||
|
||
const msg =
|
||
typeof __DEV__ !== 'undefined' && __DEV__
|
||
? t('consent.linkUnavailableDev', { baseUrl: API_BASE_URL })
|
||
: t('consent.linkUnavailable');
|
||
Alert.alert(t('common.notice'), msg);
|
||
}
|
||
|
||
// 拉取协议链接(由后端按语言下发;默认 EN)
|
||
useEffect(() => {
|
||
void refreshLegalLinks();
|
||
}, []);
|
||
|
||
const bgDecorationHeight = isTablet ? Math.round(height * 0.55) : height * 0.6;
|
||
const bgDecorationTop = height - bgDecorationHeight;
|
||
const contentTop = bgDecorationTop + (bgDecorationHeight * 0.25);
|
||
const contentWidth = isTablet ? Math.min(640, Math.floor(width * 0.76)) : width;
|
||
const topImageWidth = isTablet ? Math.min(430, Math.floor(width * 0.48)) : 308;
|
||
const topImageHeight = isTablet ? Math.min(460, Math.floor(height * 0.45)) : 354;
|
||
const topImageMarginTop = isTablet ? 148 : 60;
|
||
const bgWidth = width + 10;
|
||
const bgLeft = -3;
|
||
const buttonSize = isTablet ? { width: 108, height: 70 } : { width: 87, height: 57 };
|
||
const bottomOffset = isTablet ? 28 : 60;
|
||
const buttonBottomGap = isTablet ? 28 : 40;
|
||
const noticeStyle = isTablet
|
||
? { fontSize: 14, lineHeight: 20, paddingHorizontal: 32, maxWidth: contentWidth }
|
||
: null;
|
||
const noticeLinkStyle = isTablet ? { fontSize: 14 } : null;
|
||
const titleStyle = isTablet ? { fontSize: 50, lineHeight: 60 } : null;
|
||
const subtitleStyle = isTablet ? { marginTop: 10, fontSize: 18, lineHeight: 24 } : null;
|
||
|
||
return (
|
||
<View style={styles.container}>
|
||
{/* 中间的背景装饰 SVG (现在放在上面,作为上层) */}
|
||
<View style={[styles.bgDecorationContainer, { bottom: 0, left: bgLeft }]}>
|
||
<FlowersBg width={bgWidth} height={bgDecorationHeight} preserveAspectRatio="none" />
|
||
</View>
|
||
|
||
{/* 顶部的花图片 (现在放在下面,作为下层) */}
|
||
<View style={[styles.topImageContainer, { marginTop: topImageMarginTop }]}>
|
||
<Image
|
||
source={require('../../assets/images/index/index_flowers.png')}
|
||
style={[styles.topImage, { width: topImageWidth, height: topImageHeight }]}
|
||
resizeMode="contain"
|
||
/>
|
||
</View>
|
||
|
||
{/* 文案内容:主標題兩行 + 可選二級標題(字號更小、顏色更淺);繁中為元件內常數,其餘用 i18n */}
|
||
<View style={[styles.contentContainer, { position: 'absolute', top: contentTop, width: contentWidth }]}>
|
||
<Text style={[styles.titleText, titleStyle]}>
|
||
{title}
|
||
{'\n'}
|
||
{subtitle}
|
||
</Text>
|
||
{subtitleSecondary ? (
|
||
<Text style={[styles.consentSubtitleSecondary, subtitleStyle]}>{subtitleSecondary}</Text>
|
||
) : null}
|
||
</View>
|
||
|
||
<SafeAreaView style={[styles.bottomContainer, { bottom: bottomOffset, width: contentWidth }]} edges={['bottom']}>
|
||
{showConsent && (
|
||
<>
|
||
<TouchableOpacity
|
||
onPress={handleAgree}
|
||
activeOpacity={0.8}
|
||
style={[styles.buttonWrapper, { marginBottom: buttonBottomGap }]}
|
||
accessibilityRole="button"
|
||
accessibilityLabel={t('consent.agree')}
|
||
>
|
||
<WelcomeBtn width={buttonSize.width} height={buttonSize.height} />
|
||
</TouchableOpacity>
|
||
|
||
<Text style={[styles.noticeText, noticeStyle]}>
|
||
<Trans
|
||
i18nKey="consent.noticeRich"
|
||
values={{
|
||
privacyLabel: t('consent.privacy'),
|
||
termsLabel: t('consent.terms'),
|
||
privacySuffix: !links.privacy && linksLoading ? t('consent.linkLoadingSuffix') : '',
|
||
termsSuffix: !links.terms && linksLoading ? t('consent.linkLoadingSuffix') : '',
|
||
}}
|
||
components={{
|
||
privacy: (
|
||
<Text
|
||
style={[styles.noticeLinkText, noticeLinkStyle, !links.privacy && styles.noticeLinkTextDisabled]}
|
||
onPress={() => void handleOpenLegal('privacy')}
|
||
suppressHighlighting
|
||
/>
|
||
),
|
||
terms: (
|
||
<Text
|
||
style={[styles.noticeLinkText, noticeLinkStyle, !links.terms && styles.noticeLinkTextDisabled]}
|
||
onPress={() => void handleOpenLegal('terms')}
|
||
suppressHighlighting
|
||
/>
|
||
),
|
||
}}
|
||
/>
|
||
</Text>
|
||
</>
|
||
)}
|
||
</SafeAreaView>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
width: '100%',
|
||
height: '100%',
|
||
alignSelf: 'stretch',
|
||
backgroundColor: '#F5D3B5', // 匹配 Figma 背景色
|
||
alignItems: 'center',
|
||
},
|
||
topImageContainer: {
|
||
zIndex: 1, // 降低层级
|
||
},
|
||
topImage: {
|
||
width: 308,
|
||
height: 354,
|
||
},
|
||
bgDecorationContainer: {
|
||
position: 'absolute',
|
||
zIndex: 2, // 提高层级,使其覆盖在图片之上
|
||
},
|
||
contentContainer: {
|
||
alignItems: 'center',
|
||
zIndex: 3,
|
||
},
|
||
titleText: {
|
||
fontSize: 30,
|
||
lineHeight: 42,
|
||
color: '#772F00', // 匹配 Figma 文字颜色
|
||
textAlign: 'center',
|
||
fontWeight: '600',
|
||
fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : 'serif',
|
||
},
|
||
consentSubtitleSecondary: {
|
||
marginTop: 12,
|
||
fontSize: 16,
|
||
lineHeight: 22,
|
||
color: 'rgba(119, 47, 0, 0.6)',
|
||
textAlign: 'center',
|
||
fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : 'serif',
|
||
},
|
||
bottomContainer: {
|
||
position: 'absolute',
|
||
alignItems: 'center',
|
||
zIndex: 4,
|
||
},
|
||
buttonWrapper: {
|
||
marginBottom: 40,
|
||
},
|
||
noticeText: {
|
||
marginTop: 10,
|
||
paddingHorizontal: 28,
|
||
fontSize: 12,
|
||
lineHeight: 16,
|
||
textAlign: 'center',
|
||
color: 'rgba(119, 47, 0, 0.45)',
|
||
},
|
||
noticeLinkText: {
|
||
fontSize: 12,
|
||
// 颜色区分:协议链接更醒目
|
||
color: 'rgba(119, 47, 0, 0.75)',
|
||
textDecorationLine: 'underline',
|
||
fontWeight: '600',
|
||
},
|
||
noticeLinkTextDisabled: {
|
||
opacity: 0.55,
|
||
},
|
||
});
|