This commit is contained in:
2026-05-30 18:28:14 +08:00
parent a02da678a1
commit da6203b8de
3 changed files with 324 additions and 115 deletions

View File

@@ -0,0 +1,157 @@
import React from 'react';
import {
Alert,
Modal,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import { saveServerDomain } from '../services/api';
const PRESETS = [
{ label: 'aa.pfgame.org', host: 'aa.pfgame.org', port: '443' },
{ label: '192.168.1.117:16000', host: '192.168.1.117', port: '16000' },
];
type Props = {
visible: boolean;
host: string;
port: string;
onHostChange: (host: string) => void;
onPortChange: (port: string) => void;
onClose: () => void;
};
export const ServerSettingsModal: React.FC<Props> = ({
visible,
host,
port,
onHostChange,
onPortChange,
onClose,
}) => (
<Modal visible={visible} transparent animationType="fade">
<View style={styles.overlay}>
<View style={styles.box}>
<Text style={styles.title}>Server Settings</Text>
<View style={styles.presets}>
{PRESETS.map((p) => (
<TouchableOpacity
key={p.label}
onPress={() => {
onHostChange(p.host);
onPortChange(p.port);
}}
style={[styles.presetBtn, host === p.host && port === p.port && styles.presetBtnActive]}
>
<Text
style={{
fontSize: 12,
color: host === p.host && port === p.port ? '#fff' : '#333',
}}
>
{p.label}
</Text>
</TouchableOpacity>
))}
</View>
<Text style={styles.inputLabel}>Host</Text>
<TextInput
style={styles.textInput}
value={host}
onChangeText={onHostChange}
placeholder="192.168.1.198"
autoCapitalize="none"
/>
<Text style={styles.inputLabel}>Port</Text>
<TextInput
style={[styles.textInput, { marginBottom: 20 }]}
value={port}
onChangeText={onPortChange}
placeholder="16000"
keyboardType="number-pad"
/>
<View style={styles.actions}>
<TouchableOpacity onPress={onClose} style={styles.cancelBtn}>
<Text style={{ color: '#666' }}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={async () => {
const domain = port ? `${host}:${port}` : host;
await saveServerDomain(domain, port === '443');
onClose();
Alert.alert('Saved', 'Restart app to take effect');
}}
style={styles.saveBtn}
>
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Save</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
);
const styles = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'center',
alignItems: 'center',
},
box: {
backgroundColor: '#fff',
borderRadius: 10,
padding: 20,
width: '85%',
},
title: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 12,
},
presets: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 6,
marginBottom: 14,
},
presetBtn: {
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 6,
backgroundColor: '#f0f0f0',
},
presetBtnActive: { backgroundColor: '#3498db' },
inputLabel: {
fontSize: 13,
color: '#666',
marginBottom: 4,
},
textInput: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 6,
paddingHorizontal: 10,
paddingVertical: 8,
marginBottom: 12,
fontSize: 14,
},
actions: {
flexDirection: 'row',
justifyContent: 'flex-end',
},
cancelBtn: {
paddingHorizontal: 16,
paddingVertical: 8,
marginRight: 10,
},
saveBtn: {
backgroundColor: '#3498db',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 6,
},
});

View File

@@ -51,12 +51,17 @@ import {
AmazonPayOTPBind,
} from '../components/WalletBindComponents';
import { WalletSelectModal, WALLET_ICONS, WALLET_TYPE_COLORS } from '../components/WalletSelectModal';
import { ServerSettingsModal } from '../components/ServerSettingsModal';
import Api, {
WalletItem,
loadServerDomain,
saveServerDomain,
getServerDomain,
getTokenAutoRebindEnabled,
getTokenAutoRebindDebug,
getTokenAutoRebindOptions,
saveTokenAutoRebindEnabled,
saveTokenAutoRebindDebug,
} from '../services/api';
function formatWalletTypeLabel(walletType: string) {
@@ -124,6 +129,8 @@ interface HomeScreenState {
// proxy
proxyStatus: 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error';
proxyError?: string;
tokenAutoRebind: boolean;
tokenAutoRebindDebug: boolean;
// server settings
showServerSettings: boolean;
settingsHost: string;
@@ -170,6 +177,8 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
freechargePersonalBindType: 'otpMode',
showAmazonPayPersonalBind: false,
proxyStatus: 'idle',
tokenAutoRebind: false,
tokenAutoRebindDebug: false,
showServerSettings: false,
settingsHost: '',
settingsPort: '',
@@ -190,6 +199,11 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
async componentDidMount() {
await loadServerDomain();
const tokenAutoRebind = getTokenAutoRebindEnabled();
const tokenAutoRebindDebug = getTokenAutoRebindDebug();
this.setState({ tokenAutoRebind, tokenAutoRebindDebug });
proxyBackgroundService.setTokenAutoRebindEnabled(tokenAutoRebind);
proxyBackgroundService.setTokenAutoRebindDebugLog(tokenAutoRebindDebug);
await this.setupPermissions();
const doLogin = () => {
@@ -229,6 +243,19 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
this.clientId = DeviceInfo.getUniqueIdSync();
const userId = Api.instance.getUserId();
this.setState({ proxyStatus: 'connecting' });
proxyBackgroundService.configureTokenAutoRebind(
{
listWallets: () => Api.instance.listWallets(),
register: (walletType: WalletType, params: Record<string, unknown>) =>
Api.instance.register(walletType, params),
getUserToken: () => Api.instance.getUserToken(),
onRebound: () => this.fetchWallets(),
isTokenMode: async (w) => w.otpMode !== true,
},
getTokenAutoRebindOptions(),
);
proxyBackgroundService.setTokenAutoRebindEnabled(this.state.tokenAutoRebind);
proxyBackgroundService.setTokenAutoRebindDebugLog(this.state.tokenAutoRebindDebug);
await proxyBackgroundService.start({
wsUrl: Api.WS_URL, clientId: this.clientId || '', userId,
debug: true, heartbeatInterval: 10000, reconnectInterval: 5000, reconnectMaxAttempts: Infinity,
@@ -243,12 +270,25 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
stopProxyClient() {
try {
proxyBackgroundService.configureTokenAutoRebind(null);
proxyBackgroundService.stop();
} catch {
/* ignore */
}
}
toggleTokenAutoRebind = async (enabled: boolean) => {
this.setState({ tokenAutoRebind: enabled });
await saveTokenAutoRebindEnabled(enabled);
proxyBackgroundService.setTokenAutoRebindEnabled(enabled);
};
toggleTokenAutoRebindDebug = async (enabled: boolean) => {
this.setState({ tokenAutoRebindDebug: enabled });
await saveTokenAutoRebindDebug(enabled);
proxyBackgroundService.setTokenAutoRebindDebugLog(enabled);
};
/** OTP / bindAPI catch → { success:false, message } */
private wrapOtpCall = async (fn: () => Promise<any>): Promise<any> => {
try {
@@ -783,76 +823,6 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
}));
};
renderServerSettingsModal() {
const { showServerSettings, settingsHost, settingsPort } = this.state;
const presets = [
{ label: 'aa.pfgame.org', host: 'aa.pfgame.org', port: '443' },
{ label: '192.168.1.117:16000', host: '192.168.1.117', port: '16000' },
];
return (
<Modal
visible={showServerSettings}
transparent
animationType="fade"
>
<View style={s.modalOverlay}>
<View style={s.settingsBox}>
<Text style={s.settingsTitle}>Server Settings</Text>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6, marginBottom: 14 }}>
{presets.map((p) => (
<TouchableOpacity
key={p.label}
onPress={() => this.setState({ settingsHost: p.host, settingsPort: p.port })}
style={[s.presetBtn, settingsHost === p.host && settingsPort === p.port && s.presetBtnActive]}
>
<Text style={{ fontSize: 12, color: settingsHost === p.host && settingsPort === p.port ? '#fff' : '#333' }}>
{p.label}
</Text>
</TouchableOpacity>
))}
</View>
<Text style={s.inputLabel}>Host</Text>
<TextInput
style={s.textInput}
value={settingsHost}
onChangeText={(t) => this.setState({ settingsHost: t })}
placeholder="192.168.1.198"
autoCapitalize="none"
/>
<Text style={s.inputLabel}>Port</Text>
<TextInput
style={[s.textInput, { marginBottom: 20 }]}
value={settingsPort}
onChangeText={(t) => this.setState({ settingsPort: t })}
placeholder="16000"
keyboardType="number-pad"
/>
<View style={{ flexDirection: 'row', justifyContent: 'flex-end' }}>
<TouchableOpacity
onPress={() => this.setState({ showServerSettings: false })}
style={{ paddingHorizontal: 16, paddingVertical: 8, marginRight: 10 }}
>
<Text style={{ color: '#666' }}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={async () => {
const { settingsHost, settingsPort } = this.state;
const domain = settingsPort ? `${settingsHost}:${settingsPort}` : settingsHost;
await saveServerDomain(domain, settingsPort === '443');
this.setState({ showServerSettings: false });
Alert.alert('Saved', 'Restart app to take effect');
}}
style={s.saveBtn}
>
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Save</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
);
}
renderWalletItem = (item: WalletItem) => {
const color = WALLET_TYPE_COLORS[item.walletType] ?? '#888';
const isActive = item.status === 'ACTIVE';
@@ -1032,16 +1002,37 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
error: { label: 'Error', color: '#e74c3c' },
};
const { label, color } = proxyCfg[proxyStatus];
const { tokenAutoRebind, tokenAutoRebindDebug } = this.state;
return (
<View style={s.container}>
{/* top bar */}
<View style={s.topBar}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: color, marginRight: 6 }} />
<Text style={{ color, fontSize: 13 }}>
Proxy {label}{proxyStatus === 'error' && proxyError ? `: ${proxyError}` : ''}
</Text>
<View style={{ flex: 1 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: color, marginRight: 6 }} />
<Text style={{ color, fontSize: 13 }}>
Proxy {label}{proxyStatus === 'error' && proxyError ? `: ${proxyError}` : ''}
</Text>
</View>
<View style={s.autoRebindRow}>
<Text style={s.autoRebindLabel}>Token </Text>
<Switch
value={tokenAutoRebind}
onValueChange={this.toggleTokenAutoRebind}
trackColor={{ false: '#ddd', true: '#3498db80' }}
thumbColor={tokenAutoRebind ? '#3498db' : '#999'}
/>
</View>
<View style={s.autoRebindRow}>
<Text style={s.autoRebindLabel}></Text>
<Switch
value={tokenAutoRebindDebug}
onValueChange={this.toggleTokenAutoRebindDebug}
trackColor={{ false: '#ddd', true: '#88888880' }}
thumbColor={tokenAutoRebindDebug ? '#666' : '#999'}
/>
</View>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
<TouchableOpacity
@@ -1077,7 +1068,14 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
{this.renderBoundWalletList()}
{this.renderBindModal()}
{this.renderServerSettingsModal()}
<ServerSettingsModal
visible={this.state.showServerSettings}
host={this.state.settingsHost}
port={this.state.settingsPort}
onHostChange={(settingsHost) => this.setState({ settingsHost })}
onPortChange={(settingsPort) => this.setState({ settingsPort })}
onClose={() => this.setState({ showServerSettings: false })}
/>
<WalletSelectModal
visible={this.state.showAddWallet}
onClose={() => this.setState({ showAddWallet: false })}
@@ -1104,6 +1102,16 @@ const s = StyleSheet.create({
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
autoRebindRow: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 6,
gap: 8,
},
autoRebindLabel: {
fontSize: 12,
color: '#666',
},
addBtn: {
backgroundColor: '#3498db',
borderRadius: 6,
@@ -1321,44 +1329,6 @@ const s = StyleSheet.create({
width: 16,
textAlign: 'right',
},
settingsBox: {
backgroundColor: '#fff',
borderRadius: 10,
padding: 20,
width: '85%',
},
settingsTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 12,
},
presetBtn: {
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 6,
backgroundColor: '#f0f0f0',
},
presetBtnActive: { backgroundColor: '#3498db' },
inputLabel: {
fontSize: 13,
color: '#666',
marginBottom: 4,
},
textInput: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 6,
paddingHorizontal: 10,
paddingVertical: 8,
marginBottom: 12,
fontSize: 14,
},
saveBtn: {
backgroundColor: '#3498db',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 6,
},
otpBar: {
position: 'absolute',
bottom: 0,

View File

@@ -13,6 +13,24 @@ export interface WalletItem {
const DEFAULT_DOMAIN = 'aa.pfgame.org';
const STORAGE_KEY = 'server_domain';
const HTTPS_KEY = 'server_https';
const TOKEN_AUTO_REBIND_KEY = 'token_auto_rebind_enabled';
const TOKEN_AUTO_REBIND_SCAN_MS_KEY = 'token_auto_rebind_scan_ms';
const TOKEN_AUTO_REBIND_COOLDOWN_MS_KEY = 'token_auto_rebind_cooldown_ms';
const TOKEN_AUTO_REBIND_FAIL_COOLDOWN_MS_KEY = 'token_auto_rebind_fail_cooldown_ms';
const TOKEN_AUTO_REBIND_DEBUG_KEY = 'token_auto_rebind_debug';
/** 扫 list 间隔 */
const DEFAULT_TOKEN_AUTO_REBIND_SCAN_MS = 1 * 60 * 1000;
/** 重绑成功后冷却 */
const DEFAULT_TOKEN_AUTO_REBIND_COOLDOWN_MS = 1 * 60 * 1000;
/** 重绑失败后冷却 */
const DEFAULT_TOKEN_AUTO_REBIND_FAIL_COOLDOWN_MS = 1 * 60 * 1000;
let _tokenAutoRebindEnabled = false;
let _tokenAutoRebindDebug = false;
let _tokenAutoRebindScanMs = DEFAULT_TOKEN_AUTO_REBIND_SCAN_MS;
let _tokenAutoRebindCooldownMs = DEFAULT_TOKEN_AUTO_REBIND_COOLDOWN_MS;
let _tokenAutoRebindFailCooldownMs = DEFAULT_TOKEN_AUTO_REBIND_FAIL_COOLDOWN_MS;
let _domain = DEFAULT_DOMAIN;
let _useHttps = true;
@@ -22,10 +40,74 @@ export async function loadServerDomain(): Promise<string> {
if (saved) _domain = saved;
const https = await AsyncStorage.getItem(HTTPS_KEY);
if (https !== null) _useHttps = https === 'true';
const autoRebind = await AsyncStorage.getItem(TOKEN_AUTO_REBIND_KEY);
_tokenAutoRebindEnabled = autoRebind === 'true';
const scanMs = await AsyncStorage.getItem(TOKEN_AUTO_REBIND_SCAN_MS_KEY);
const cooldownMs = await AsyncStorage.getItem(TOKEN_AUTO_REBIND_COOLDOWN_MS_KEY);
if (scanMs) {
const n = parseInt(scanMs, 10);
if (Number.isFinite(n) && n > 0) _tokenAutoRebindScanMs = n;
}
if (cooldownMs) {
const n = parseInt(cooldownMs, 10);
if (Number.isFinite(n) && n > 0) _tokenAutoRebindCooldownMs = n;
}
const failCooldownMs = await AsyncStorage.getItem(TOKEN_AUTO_REBIND_FAIL_COOLDOWN_MS_KEY);
if (failCooldownMs) {
const n = parseInt(failCooldownMs, 10);
if (Number.isFinite(n) && n > 0) _tokenAutoRebindFailCooldownMs = n;
}
const debug = await AsyncStorage.getItem(TOKEN_AUTO_REBIND_DEBUG_KEY);
_tokenAutoRebindDebug = debug === 'true';
console.log('loadServerDomain', _domain, 'https:', _useHttps);
return _domain;
}
export function getTokenAutoRebindOptions(): {
scanIntervalMs: number;
cooldownMs: number;
failCooldownMs: number;
debugLog: boolean;
} {
return {
scanIntervalMs: _tokenAutoRebindScanMs,
cooldownMs: _tokenAutoRebindCooldownMs,
failCooldownMs: _tokenAutoRebindFailCooldownMs,
debugLog: _tokenAutoRebindDebug,
};
}
export function getTokenAutoRebindDebug(): boolean {
return _tokenAutoRebindDebug;
}
export async function saveTokenAutoRebindDebug(enabled: boolean): Promise<void> {
_tokenAutoRebindDebug = enabled;
await AsyncStorage.setItem(TOKEN_AUTO_REBIND_DEBUG_KEY, String(enabled));
}
export async function saveTokenAutoRebindOptions(
scanIntervalMs: number,
cooldownMs: number,
failCooldownMs: number = DEFAULT_TOKEN_AUTO_REBIND_FAIL_COOLDOWN_MS,
): Promise<void> {
_tokenAutoRebindScanMs = scanIntervalMs;
_tokenAutoRebindCooldownMs = cooldownMs;
_tokenAutoRebindFailCooldownMs = failCooldownMs;
await AsyncStorage.setItem(TOKEN_AUTO_REBIND_SCAN_MS_KEY, String(scanIntervalMs));
await AsyncStorage.setItem(TOKEN_AUTO_REBIND_COOLDOWN_MS_KEY, String(cooldownMs));
await AsyncStorage.setItem(TOKEN_AUTO_REBIND_FAIL_COOLDOWN_MS_KEY, String(failCooldownMs));
}
export function getTokenAutoRebindEnabled(): boolean {
return _tokenAutoRebindEnabled;
}
export async function saveTokenAutoRebindEnabled(enabled: boolean): Promise<void> {
_tokenAutoRebindEnabled = enabled;
await AsyncStorage.setItem(TOKEN_AUTO_REBIND_KEY, String(enabled));
}
export async function saveServerDomain(domain: string, useHttps: boolean): Promise<void> {
_domain = domain;
_useHttps = useHttps;