fix sms otp retriver
This commit is contained in:
@@ -6,7 +6,9 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
Platform,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { normalizeHintPhone, requestPhoneNumberHint } from '../utils/smsRetriever';
|
||||||
import { WalletType } from 'rnwalletman';
|
import { WalletType } from 'rnwalletman';
|
||||||
import { useOTPBind } from '../hooks/useOTPBind';
|
import { useOTPBind } from '../hooks/useOTPBind';
|
||||||
|
|
||||||
@@ -57,6 +59,29 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
|
|||||||
|
|
||||||
const isLoading = state.loading || state.step === 'processing';
|
const isLoading = state.loading || state.step === 'processing';
|
||||||
|
|
||||||
|
const pickPhoneFromSystem = async () => {
|
||||||
|
if (Platform.OS !== 'android' || isLoading) return;
|
||||||
|
try {
|
||||||
|
const raw = await requestPhoneNumberHint();
|
||||||
|
const digits = normalizeHintPhone(raw, mobileLength);
|
||||||
|
if (digits.length === mobileLength) {
|
||||||
|
actions.setMobile(digits);
|
||||||
|
if (state.errorMessage) actions.clearError();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* 用户取消 */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPickPhone = () => {
|
||||||
|
if (Platform.OS !== 'android') return null;
|
||||||
|
return (
|
||||||
|
<TouchableOpacity style={styles.linkBtn} onPress={pickPhoneFromSystem} disabled={isLoading}>
|
||||||
|
<Text style={[styles.linkText, isLoading && styles.linkTextDisabled]}>从系统选择号码</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderResendOtp = () => {
|
const renderResendOtp = () => {
|
||||||
if (!state.otpSent) return null;
|
if (!state.otpSent) return null;
|
||||||
const disabled = isLoading || state.resendCountdown > 0;
|
const disabled = isLoading || state.resendCountdown > 0;
|
||||||
@@ -98,6 +123,7 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
|
|||||||
}}
|
}}
|
||||||
editable={!state.formStarted && !isLoading}
|
editable={!state.formStarted && !isLoading}
|
||||||
/>
|
/>
|
||||||
|
{!state.formStarted && renderPickPhone()}
|
||||||
{state.formStarted && state.passwordRequired && (
|
{state.formStarted && state.passwordRequired && (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.label}>{passwordLabel}</Text>
|
<Text style={styles.label}>{passwordLabel}</Text>
|
||||||
@@ -206,6 +232,7 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
|
|||||||
}}
|
}}
|
||||||
editable={!isLoading}
|
editable={!isLoading}
|
||||||
/>
|
/>
|
||||||
|
{renderPickPhone()}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.btn, isLoading && styles.btnDisabled]}
|
style={[styles.btn, isLoading && styles.btnDisabled]}
|
||||||
onPress={actions.requestOTP}
|
onPress={actions.requestOTP}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
import { WalletType } from 'rnwalletman';
|
import { WalletType } from 'rnwalletman';
|
||||||
|
import { startOtpSmsListener } from '../utils/smsRetriever';
|
||||||
|
|
||||||
export interface OTPBindCallbacks {
|
export interface OTPBindCallbacks {
|
||||||
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
|
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
|
||||||
@@ -57,6 +59,8 @@ export function useOTPBind(
|
|||||||
const [otpData, setOtpData] = useState<any>(null);
|
const [otpData, setOtpData] = useState<any>(null);
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
const [resendCountdown, setResendCountdown] = useState(0);
|
const [resendCountdown, setResendCountdown] = useState(0);
|
||||||
|
const [smsListenKey, setSmsListenKey] = useState(0);
|
||||||
|
const smsStopRef = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
const { onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug = false } = callbacks;
|
const { onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug = false } = callbacks;
|
||||||
const {
|
const {
|
||||||
@@ -73,6 +77,14 @@ export function useOTPBind(
|
|||||||
if (initialMobile) setMobile(initialMobile);
|
if (initialMobile) setMobile(initialMobile);
|
||||||
}, [initialMobile]);
|
}, [initialMobile]);
|
||||||
|
|
||||||
|
const log = (...args: any[]) => {
|
||||||
|
if (isDebug) console.log(`[${walletType}]`, ...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
const error = (...args: any[]) => {
|
||||||
|
if (isDebug) console.error(`[${walletType}]`, ...args);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (resendCountdown <= 0) return;
|
if (resendCountdown <= 0) return;
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
@@ -81,18 +93,43 @@ export function useOTPBind(
|
|||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, [resendCountdown]);
|
}, [resendCountdown]);
|
||||||
|
|
||||||
|
const shouldListenSms =
|
||||||
|
Platform.OS === 'android' &&
|
||||||
|
(step === 'otp' || (passwordBeforeOtp && otpSent && formStarted));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldListenSms) {
|
||||||
|
smsStopRef.current?.();
|
||||||
|
smsStopRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cancelled = false;
|
||||||
|
(async () => {
|
||||||
|
smsStopRef.current?.();
|
||||||
|
const stop = await startOtpSmsListener(otpLength, (code) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setOtp(code);
|
||||||
|
log('OTP from SMS Retriever:', code);
|
||||||
|
});
|
||||||
|
if (cancelled) {
|
||||||
|
stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
smsStopRef.current = stop;
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
smsStopRef.current?.();
|
||||||
|
smsStopRef.current = null;
|
||||||
|
};
|
||||||
|
}, [shouldListenSms, smsListenKey, otpLength]);
|
||||||
|
|
||||||
|
useEffect(() => () => smsStopRef.current?.(), []);
|
||||||
|
|
||||||
const startResendCooldown = () => setResendCountdown(resendCooldown);
|
const startResendCooldown = () => setResendCountdown(resendCooldown);
|
||||||
|
|
||||||
const clearError = () => setErrorMessage('');
|
const clearError = () => setErrorMessage('');
|
||||||
|
|
||||||
const log = (...args: any[]) => {
|
|
||||||
if (isDebug) console.log(`[${walletType}]`, ...args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const error = (...args: any[]) => {
|
|
||||||
if (isDebug) console.error(`[${walletType}]`, ...args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const requestOTP = async () => {
|
const requestOTP = async () => {
|
||||||
const withPassword = passwordBeforeOtp && formStarted && passwordRequired;
|
const withPassword = passwordBeforeOtp && formStarted && passwordRequired;
|
||||||
const isResend = otpSent && formStarted;
|
const isResend = otpSent && formStarted;
|
||||||
@@ -136,6 +173,7 @@ export function useOTPBind(
|
|||||||
} else {
|
} else {
|
||||||
setPasswordRequired(!!response.data?.needPassword || withPassword);
|
setPasswordRequired(!!response.data?.needPassword || withPassword);
|
||||||
setOtpSent(true);
|
setOtpSent(true);
|
||||||
|
setSmsListenKey((k) => k + 1);
|
||||||
startResendCooldown();
|
startResendCooldown();
|
||||||
if (!passwordBeforeOtp) {
|
if (!passwordBeforeOtp) {
|
||||||
setStep('otp');
|
setStep('otp');
|
||||||
@@ -216,6 +254,9 @@ export function useOTPBind(
|
|||||||
setOtpData(null);
|
setOtpData(null);
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
setResendCountdown(0);
|
setResendCountdown(0);
|
||||||
|
setSmsListenKey(0);
|
||||||
|
smsStopRef.current?.();
|
||||||
|
smsStopRef.current = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"react-native-gesture-handler": "~2.9.0",
|
"react-native-gesture-handler": "~2.9.0",
|
||||||
"react-native-safe-area-context": "^4.4.1",
|
"react-native-safe-area-context": "^4.4.1",
|
||||||
"react-native-screens": "~3.36.0",
|
"react-native-screens": "~3.36.0",
|
||||||
|
"react-native-sms-retriever": "1.1.1",
|
||||||
"react-native-svg": "^14.1.0",
|
"react-native-svg": "^14.1.0",
|
||||||
"react-native-svg-transformer": "^1.5.3",
|
"react-native-svg-transformer": "^1.5.3",
|
||||||
"react-native-tcp-socket": "^6.4.1",
|
"react-native-tcp-socket": "^6.4.1",
|
||||||
|
|||||||
@@ -25,15 +25,10 @@ import {
|
|||||||
FreechargePersonalBindResult,
|
FreechargePersonalBindResult,
|
||||||
GooglePayBusinessBindResult,
|
GooglePayBusinessBindResult,
|
||||||
BharatPeBusinessBindResult,
|
BharatPeBusinessBindResult,
|
||||||
onSmsMessage,
|
|
||||||
startSmsListener,
|
|
||||||
stopSmsListener,
|
|
||||||
checkSmsPermission,
|
|
||||||
requestSmsPermission,
|
|
||||||
PhonePePersonalBind,
|
PhonePePersonalBind,
|
||||||
FreechargePersonalBind,
|
FreechargePersonalBind,
|
||||||
SmsMessage,
|
|
||||||
proxyBackgroundService,
|
proxyBackgroundService,
|
||||||
|
type TokenAutoRebindDeps,
|
||||||
PhonePePersonalBindResult,
|
PhonePePersonalBindResult,
|
||||||
PaytmPersonalBindResult,
|
PaytmPersonalBindResult,
|
||||||
BindErrorCode,
|
BindErrorCode,
|
||||||
@@ -58,10 +53,8 @@ import Api, {
|
|||||||
loadServerDomain,
|
loadServerDomain,
|
||||||
getServerDomain,
|
getServerDomain,
|
||||||
getTokenAutoRebindEnabled,
|
getTokenAutoRebindEnabled,
|
||||||
getTokenAutoRebindDebug,
|
|
||||||
getTokenAutoRebindOptions,
|
getTokenAutoRebindOptions,
|
||||||
saveTokenAutoRebindEnabled,
|
saveTokenAutoRebindEnabled,
|
||||||
saveTokenAutoRebindDebug,
|
|
||||||
} from '../services/api';
|
} from '../services/api';
|
||||||
|
|
||||||
function formatWalletTypeLabel(walletType: string) {
|
function formatWalletTypeLabel(walletType: string) {
|
||||||
@@ -130,7 +123,6 @@ interface HomeScreenState {
|
|||||||
proxyStatus: 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error';
|
proxyStatus: 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error';
|
||||||
proxyError?: string;
|
proxyError?: string;
|
||||||
tokenAutoRebind: boolean;
|
tokenAutoRebind: boolean;
|
||||||
tokenAutoRebindDebug: boolean;
|
|
||||||
// server settings
|
// server settings
|
||||||
showServerSettings: boolean;
|
showServerSettings: boolean;
|
||||||
settingsHost: string;
|
settingsHost: string;
|
||||||
@@ -178,7 +170,6 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
|
|||||||
showAmazonPayPersonalBind: false,
|
showAmazonPayPersonalBind: false,
|
||||||
proxyStatus: 'idle',
|
proxyStatus: 'idle',
|
||||||
tokenAutoRebind: false,
|
tokenAutoRebind: false,
|
||||||
tokenAutoRebindDebug: false,
|
|
||||||
showServerSettings: false,
|
showServerSettings: false,
|
||||||
settingsHost: '',
|
settingsHost: '',
|
||||||
settingsPort: '',
|
settingsPort: '',
|
||||||
@@ -200,11 +191,8 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
|
|||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
await loadServerDomain();
|
await loadServerDomain();
|
||||||
const tokenAutoRebind = getTokenAutoRebindEnabled();
|
const tokenAutoRebind = getTokenAutoRebindEnabled();
|
||||||
const tokenAutoRebindDebug = getTokenAutoRebindDebug();
|
this.setState({ tokenAutoRebind });
|
||||||
this.setState({ tokenAutoRebind, tokenAutoRebindDebug });
|
|
||||||
proxyBackgroundService.setTokenAutoRebindEnabled(tokenAutoRebind);
|
proxyBackgroundService.setTokenAutoRebindEnabled(tokenAutoRebind);
|
||||||
proxyBackgroundService.setTokenAutoRebindDebugLog(tokenAutoRebindDebug);
|
|
||||||
await this.setupPermissions();
|
|
||||||
|
|
||||||
const doLogin = () => {
|
const doLogin = () => {
|
||||||
Api.instance.login('test123', '123456').then(async () => {
|
Api.instance.login('test123', '123456').then(async () => {
|
||||||
@@ -221,7 +209,6 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.stopProxyClient();
|
this.stopProxyClient();
|
||||||
stopSmsListener();
|
|
||||||
this.appStateSubscription?.remove();
|
this.appStateSubscription?.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,14 +216,17 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
|
|||||||
if (nextAppState === 'active') this.fetchWallets();
|
if (nextAppState === 'active') this.fetchWallets();
|
||||||
};
|
};
|
||||||
|
|
||||||
async setupPermissions() {
|
buildTokenAutoRebindDeps = (): TokenAutoRebindDeps => ({
|
||||||
const hasSms = await checkSmsPermission();
|
listWallets: () => Api.instance.listWallets(),
|
||||||
if (!hasSms) await requestSmsPermission();
|
register: (_wallet, walletType: WalletType, params: Record<string, unknown>) =>
|
||||||
startSmsListener();
|
Api.instance.register(walletType, params),
|
||||||
onSmsMessage((msg: SmsMessage) => {
|
getUserToken: () => Api.instance.getUserToken(),
|
||||||
console.log('[SMS]', msg.address, msg.body);
|
onRebound: () => this.fetchWallets(),
|
||||||
|
isActive: async (w) => w.status === 'ACTIVE' || w.otpMode === true,
|
||||||
|
log: this.state.tokenAutoRebind
|
||||||
|
? (...args: unknown[]) => console.log('[TokenAutoRebind]', ...args)
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
async startProxyClient() {
|
async startProxyClient() {
|
||||||
try {
|
try {
|
||||||
@@ -244,18 +234,10 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
|
|||||||
const userId = Api.instance.getUserId();
|
const userId = Api.instance.getUserId();
|
||||||
this.setState({ proxyStatus: 'connecting' });
|
this.setState({ proxyStatus: 'connecting' });
|
||||||
proxyBackgroundService.configureTokenAutoRebind(
|
proxyBackgroundService.configureTokenAutoRebind(
|
||||||
{
|
this.buildTokenAutoRebindDeps(),
|
||||||
listWallets: () => Api.instance.listWallets(),
|
|
||||||
register: (_wallet, walletType: WalletType, params: Record<string, unknown>) =>
|
|
||||||
Api.instance.register(walletType, params),
|
|
||||||
getUserToken: () => Api.instance.getUserToken(),
|
|
||||||
onRebound: () => this.fetchWallets(),
|
|
||||||
isActive: async (w) => w.status === 'ACTIVE' || w.otpMode === true,
|
|
||||||
},
|
|
||||||
getTokenAutoRebindOptions(),
|
getTokenAutoRebindOptions(),
|
||||||
);
|
);
|
||||||
proxyBackgroundService.setTokenAutoRebindEnabled(this.state.tokenAutoRebind);
|
proxyBackgroundService.setTokenAutoRebindEnabled(this.state.tokenAutoRebind);
|
||||||
proxyBackgroundService.setTokenAutoRebindDebugLog(this.state.tokenAutoRebindDebug);
|
|
||||||
await proxyBackgroundService.start({
|
await proxyBackgroundService.start({
|
||||||
wsUrl: Api.WS_URL, clientId: this.clientId || '', userId,
|
wsUrl: Api.WS_URL, clientId: this.clientId || '', userId,
|
||||||
debug: true, heartbeatInterval: 10000, reconnectInterval: 5000, reconnectMaxAttempts: Infinity,
|
debug: true, heartbeatInterval: 10000, reconnectInterval: 5000, reconnectMaxAttempts: Infinity,
|
||||||
@@ -278,17 +260,16 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleTokenAutoRebind = async (enabled: boolean) => {
|
toggleTokenAutoRebind = async (enabled: boolean) => {
|
||||||
this.setState({ tokenAutoRebind: enabled });
|
this.setState({ tokenAutoRebind: enabled }, () => {
|
||||||
|
proxyBackgroundService.configureTokenAutoRebind(
|
||||||
|
this.buildTokenAutoRebindDeps(),
|
||||||
|
getTokenAutoRebindOptions(),
|
||||||
|
);
|
||||||
|
});
|
||||||
await saveTokenAutoRebindEnabled(enabled);
|
await saveTokenAutoRebindEnabled(enabled);
|
||||||
proxyBackgroundService.setTokenAutoRebindEnabled(enabled);
|
proxyBackgroundService.setTokenAutoRebindEnabled(enabled);
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleTokenAutoRebindDebug = async (enabled: boolean) => {
|
|
||||||
this.setState({ tokenAutoRebindDebug: enabled });
|
|
||||||
await saveTokenAutoRebindDebug(enabled);
|
|
||||||
proxyBackgroundService.setTokenAutoRebindDebugLog(enabled);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** OTP / bind:API catch → { success:false, message } */
|
/** OTP / bind:API catch → { success:false, message } */
|
||||||
private wrapOtpCall = async (fn: () => Promise<any>): Promise<any> => {
|
private wrapOtpCall = async (fn: () => Promise<any>): Promise<any> => {
|
||||||
try {
|
try {
|
||||||
@@ -1002,7 +983,7 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
|
|||||||
error: { label: 'Error', color: '#e74c3c' },
|
error: { label: 'Error', color: '#e74c3c' },
|
||||||
};
|
};
|
||||||
const { label, color } = proxyCfg[proxyStatus];
|
const { label, color } = proxyCfg[proxyStatus];
|
||||||
const { tokenAutoRebind, tokenAutoRebindDebug } = this.state;
|
const { tokenAutoRebind } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={s.container}>
|
<View style={s.container}>
|
||||||
@@ -1024,15 +1005,6 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
|
|||||||
thumbColor={tokenAutoRebind ? '#3498db' : '#999'}
|
thumbColor={tokenAutoRebind ? '#3498db' : '#999'}
|
||||||
/>
|
/>
|
||||||
</View>
|
</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>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ 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_SCAN_MS_KEY = 'token_auto_rebind_scan_ms';
|
||||||
const TOKEN_AUTO_REBIND_COOLDOWN_MS_KEY = 'token_auto_rebind_cooldown_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_FAIL_COOLDOWN_MS_KEY = 'token_auto_rebind_fail_cooldown_ms';
|
||||||
const TOKEN_AUTO_REBIND_DEBUG_KEY = 'token_auto_rebind_debug';
|
|
||||||
|
|
||||||
/** 扫 list 间隔 */
|
/** 扫 list 间隔 */
|
||||||
const DEFAULT_TOKEN_AUTO_REBIND_SCAN_MS = 1 * 60 * 1000;
|
const DEFAULT_TOKEN_AUTO_REBIND_SCAN_MS = 1 * 60 * 1000;
|
||||||
/** 重绑成功后冷却 */
|
/** 重绑成功后冷却 */
|
||||||
@@ -27,7 +25,6 @@ const DEFAULT_TOKEN_AUTO_REBIND_COOLDOWN_MS = 1 * 60 * 1000;
|
|||||||
const DEFAULT_TOKEN_AUTO_REBIND_FAIL_COOLDOWN_MS = 1 * 60 * 1000;
|
const DEFAULT_TOKEN_AUTO_REBIND_FAIL_COOLDOWN_MS = 1 * 60 * 1000;
|
||||||
|
|
||||||
let _tokenAutoRebindEnabled = false;
|
let _tokenAutoRebindEnabled = false;
|
||||||
let _tokenAutoRebindDebug = false;
|
|
||||||
let _tokenAutoRebindScanMs = DEFAULT_TOKEN_AUTO_REBIND_SCAN_MS;
|
let _tokenAutoRebindScanMs = DEFAULT_TOKEN_AUTO_REBIND_SCAN_MS;
|
||||||
let _tokenAutoRebindCooldownMs = DEFAULT_TOKEN_AUTO_REBIND_COOLDOWN_MS;
|
let _tokenAutoRebindCooldownMs = DEFAULT_TOKEN_AUTO_REBIND_COOLDOWN_MS;
|
||||||
let _tokenAutoRebindFailCooldownMs = DEFAULT_TOKEN_AUTO_REBIND_FAIL_COOLDOWN_MS;
|
let _tokenAutoRebindFailCooldownMs = DEFAULT_TOKEN_AUTO_REBIND_FAIL_COOLDOWN_MS;
|
||||||
@@ -57,8 +54,6 @@ export async function loadServerDomain(): Promise<string> {
|
|||||||
const n = parseInt(failCooldownMs, 10);
|
const n = parseInt(failCooldownMs, 10);
|
||||||
if (Number.isFinite(n) && n > 0) _tokenAutoRebindFailCooldownMs = n;
|
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);
|
console.log('loadServerDomain', _domain, 'https:', _useHttps);
|
||||||
return _domain;
|
return _domain;
|
||||||
}
|
}
|
||||||
@@ -67,25 +62,14 @@ export function getTokenAutoRebindOptions(): {
|
|||||||
scanIntervalMs: number;
|
scanIntervalMs: number;
|
||||||
cooldownMs: number;
|
cooldownMs: number;
|
||||||
failCooldownMs: number;
|
failCooldownMs: number;
|
||||||
debugLog: boolean;
|
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
scanIntervalMs: _tokenAutoRebindScanMs,
|
scanIntervalMs: _tokenAutoRebindScanMs,
|
||||||
cooldownMs: _tokenAutoRebindCooldownMs,
|
cooldownMs: _tokenAutoRebindCooldownMs,
|
||||||
failCooldownMs: _tokenAutoRebindFailCooldownMs,
|
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(
|
export async function saveTokenAutoRebindOptions(
|
||||||
scanIntervalMs: number,
|
scanIntervalMs: number,
|
||||||
cooldownMs: number,
|
cooldownMs: number,
|
||||||
|
|||||||
69
utils/smsRetriever.ts
Normal file
69
utils/smsRetriever.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
|
type SmsRetrieverModule = {
|
||||||
|
requestPhoneNumber: () => Promise<string>;
|
||||||
|
startSmsRetriever: () => Promise<boolean>;
|
||||||
|
addSmsListener: (callback: (event: { message?: string }) => void) => Promise<boolean>;
|
||||||
|
removeSmsListener: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getModule(): SmsRetrieverModule | null {
|
||||||
|
if (Platform.OS !== 'android') return null;
|
||||||
|
return require('react-native-sms-retriever').default as SmsRetrieverModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractOtpFromMessage(message: string, length: number): string | null {
|
||||||
|
const exact = new RegExp(`(?:^|[^\\d])(\\d{${length}})(?:[^\\d]|$)`);
|
||||||
|
const m = message.match(exact);
|
||||||
|
if (m) return m[1];
|
||||||
|
const any = message.match(/(\d{4,8})/);
|
||||||
|
if (!any) return null;
|
||||||
|
const digits = any[1];
|
||||||
|
if (digits.length >= length) return digits.slice(0, length);
|
||||||
|
if (digits.length === length) return digits;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** SMS Retriever:无需 READ_SMS,短信需含 app hash(见 Google 文档) */
|
||||||
|
export async function startOtpSmsListener(
|
||||||
|
otpLength: number,
|
||||||
|
onOtp: (otp: string) => void,
|
||||||
|
): Promise<() => void> {
|
||||||
|
const mod = getModule();
|
||||||
|
if (!mod) return () => {};
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
try {
|
||||||
|
mod.removeSmsListener();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const registered = await mod.startSmsRetriever();
|
||||||
|
if (!registered) return stop;
|
||||||
|
await mod.addSmsListener((event) => {
|
||||||
|
const msg = event.message ?? '';
|
||||||
|
const otp = extractOtpFromMessage(msg, otpLength);
|
||||||
|
if (otp) {
|
||||||
|
onOtp(otp);
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
return stop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 系统号码选择器,一次性授权,无需 READ_SMS */
|
||||||
|
export async function requestPhoneNumberHint(): Promise<string> {
|
||||||
|
const mod = getModule();
|
||||||
|
if (!mod) throw new Error('仅支持 Android');
|
||||||
|
return mod.requestPhoneNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeHintPhone(raw: string, mobileLength = 10): string {
|
||||||
|
return raw.replace(/\D/g, '').slice(-mobileLength);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user