Implement Android foreground service to keep app alive in background for real-time message sync: - Add withForegroundService expo config plugin for notification channel setup - Add BackgroundSyncManager with REALTIME, BATTERY_SAVER, and DISABLED modes - Add ForegroundServiceModule for native Android foreground service binding - Refactor backgroundService to integrate foreground service and expo-background-task Also refactor auth and profile screens to use dynamic theme colors: - Replace hardcoded THEME_COLORS with colors.primary.main, colors.background.paper, etc. - Add useResolvedColorScheme for dynamic StatusBar styling - Update NotificationSettingsScreen with sync mode selection UI BREAKING CHANGE: Switched from expo-background-fetch to expo-background-task, minimum background sync interval changed from 900s to 15min in config
187 lines
4.6 KiB
TypeScript
187 lines
4.6 KiB
TypeScript
/**
|
||
* 注册步骤1:输入邮箱
|
||
* 包含邮箱输入框和获取验证码按钮
|
||
*/
|
||
|
||
import React, { useState } from 'react';
|
||
import {
|
||
View,
|
||
Text,
|
||
StyleSheet,
|
||
TextInput,
|
||
TouchableOpacity,
|
||
} from 'react-native';
|
||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||
import { useAppColors, type AppColors } from '../../theme';
|
||
import { RegisterStepProps } from './types';
|
||
|
||
export const RegisterStep1Email: React.FC<RegisterStepProps> = ({
|
||
formData,
|
||
updateFormData,
|
||
onNext,
|
||
colors: propColors,
|
||
sendingCode,
|
||
countdown,
|
||
onSendCode,
|
||
}) => {
|
||
const colors = propColors || useAppColors();
|
||
const styles = createStyles(colors);
|
||
|
||
const [emailError, setEmailError] = useState('');
|
||
|
||
const validateEmail = (email: string): boolean => {
|
||
if (!email.trim()) {
|
||
setEmailError('请输入邮箱');
|
||
return false;
|
||
}
|
||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||
setEmailError('请输入正确的邮箱地址');
|
||
return false;
|
||
}
|
||
setEmailError('');
|
||
return true;
|
||
};
|
||
|
||
const handleSendCode = async () => {
|
||
if (!validateEmail(formData.email)) {
|
||
return;
|
||
}
|
||
if (onSendCode) {
|
||
const success = await onSendCode(formData.email);
|
||
if (success) {
|
||
onNext();
|
||
}
|
||
}
|
||
};
|
||
|
||
return (
|
||
<View style={styles.container}>
|
||
<View style={styles.formSection}>
|
||
{/* 邮箱输入框 */}
|
||
<View style={styles.inputContainer}>
|
||
<Text style={styles.inputLabel}>邮箱</Text>
|
||
<View
|
||
style={[
|
||
styles.inputWrapper,
|
||
emailError ? styles.inputWrapperError : null,
|
||
]}
|
||
>
|
||
<TextInput
|
||
style={styles.input}
|
||
placeholder="请输入邮箱地址"
|
||
placeholderTextColor={colors.text?.hint || '#999'}
|
||
value={formData.email}
|
||
onChangeText={(text) => {
|
||
updateFormData({ email: text });
|
||
if (emailError) setEmailError('');
|
||
}}
|
||
autoCapitalize="none"
|
||
autoCorrect={false}
|
||
keyboardType="email-address"
|
||
editable={!sendingCode}
|
||
/>
|
||
{formData.email.length > 0 &&
|
||
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email) && (
|
||
<MaterialCommunityIcons
|
||
name="check-circle"
|
||
size={20}
|
||
color={colors.primary.main}
|
||
style={styles.checkIcon}
|
||
/>
|
||
)}
|
||
</View>
|
||
{emailError ? (
|
||
<Text style={styles.errorText}>{emailError}</Text>
|
||
) : null}
|
||
</View>
|
||
|
||
{/* 获取验证码按钮 */}
|
||
<TouchableOpacity
|
||
style={[
|
||
styles.actionButton,
|
||
(sendingCode || (countdown ?? 0) > 0) && styles.actionButtonDisabled,
|
||
]}
|
||
onPress={handleSendCode}
|
||
disabled={sendingCode || (countdown ?? 0) > 0}
|
||
activeOpacity={0.9}
|
||
>
|
||
<Text style={styles.actionButtonText}>
|
||
{sendingCode
|
||
? '发送中...'
|
||
: (countdown ?? 0) > 0
|
||
? `${countdown}秒后重试`
|
||
: '获取验证码'}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
function createStyles(colors: AppColors) {
|
||
return StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
paddingTop: 20,
|
||
},
|
||
formSection: {
|
||
marginBottom: 24,
|
||
},
|
||
inputContainer: {
|
||
marginBottom: 24,
|
||
},
|
||
inputLabel: {
|
||
fontSize: 14,
|
||
fontWeight: '500',
|
||
color: colors.text?.secondary || '#666',
|
||
marginBottom: 10,
|
||
},
|
||
inputWrapper: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
backgroundColor: colors.background.default,
|
||
borderRadius: 14,
|
||
paddingHorizontal: 18,
|
||
height: 56,
|
||
borderWidth: 1,
|
||
borderColor: 'transparent',
|
||
},
|
||
inputWrapperError: {
|
||
borderColor: colors.error?.main || '#FF4444',
|
||
},
|
||
input: {
|
||
flex: 1,
|
||
fontSize: 16,
|
||
color: colors.text?.primary || '#333',
|
||
height: 56,
|
||
},
|
||
checkIcon: {
|
||
marginLeft: 8,
|
||
},
|
||
errorText: {
|
||
fontSize: 12,
|
||
color: colors.error?.main || '#FF4444',
|
||
marginTop: 6,
|
||
marginLeft: 4,
|
||
},
|
||
actionButton: {
|
||
height: 56,
|
||
borderRadius: 14,
|
||
backgroundColor: colors.primary.main,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
marginTop: 8,
|
||
},
|
||
actionButtonDisabled: {
|
||
opacity: 0.6,
|
||
},
|
||
actionButtonText: {
|
||
fontSize: 17,
|
||
fontWeight: '600',
|
||
color: colors.text.inverse,
|
||
},
|
||
});
|
||
}
|
||
|
||
export default RegisterStep1Email;
|