自动读取短信

This commit is contained in:
2026-05-31 01:22:29 +08:00
parent 557931892f
commit eea21cd33b
6 changed files with 64 additions and 112 deletions

View File

@@ -6,9 +6,7 @@ import {
Text,
StyleSheet,
ActivityIndicator,
Platform,
} from 'react-native';
import { normalizeHintPhone, requestPhoneNumberHint } from '../utils/smsRetriever';
import { WalletType } from 'rnwalletman';
import { useOTPBind } from '../hooks/useOTPBind';
@@ -59,29 +57,6 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
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 = () => {
if (!state.otpSent) return null;
const disabled = isLoading || state.resendCountdown > 0;
@@ -123,7 +98,6 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
}}
editable={!state.formStarted && !isLoading}
/>
{!state.formStarted && renderPickPhone()}
{state.formStarted && state.passwordRequired && (
<>
<Text style={styles.label}>{passwordLabel}</Text>
@@ -160,6 +134,7 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
{state.otpSent && (
<>
<Text style={styles.hint}>OTP sent to {state.mobile}</Text>
<Text style={styles.consentHint}></Text>
<Text style={styles.label}>Verification code</Text>
<TextInput
style={[styles.input, !!state.errorMessage && styles.inputError]}
@@ -232,7 +207,6 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
}}
editable={!isLoading}
/>
{renderPickPhone()}
<TouchableOpacity
style={[styles.btn, isLoading && styles.btnDisabled]}
onPress={actions.requestOTP}
@@ -251,6 +225,7 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
{showOtp && (
<>
<Text style={styles.hint}>OTP sent to {state.mobile}</Text>
<Text style={styles.consentHint}></Text>
{!!state.errorMessage && (
<Text style={styles.errorText}>{state.errorMessage}</Text>
)}
@@ -338,6 +313,13 @@ const styles = StyleSheet.create({
textAlign: 'center',
marginBottom: 14,
},
consentHint: {
fontSize: 11,
color: '#888',
textAlign: 'center',
marginTop: -8,
marginBottom: 12,
},
input: {
borderWidth: 1.5,
borderColor: '#e0e0e0',

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useRef } from 'react';
import { Platform } from 'react-native';
import { WalletType } from 'rnwalletman';
import { startOtpSmsListener } from '../utils/smsRetriever';
import { startSmsUserConsentListener } from '../utils/smsUserConsent';
export interface OTPBindCallbacks {
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
@@ -104,22 +104,16 @@ export function useOTPBind(
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;
})();
smsStopRef.current?.();
const stop = startSmsUserConsentListener(otpLength, (digits) => {
if (cancelled) return;
setOtp(digits);
log('SMS User Consent:', digits);
});
smsStopRef.current = stop;
return () => {
cancelled = true;
smsStopRef.current?.();
stop();
smsStopRef.current = null;
};
}, [shouldListenSms, smsListenKey, otpLength]);

View File

@@ -11,6 +11,7 @@
"postinstall": "patch-package"
},
"dependencies": {
"@eabdullazyanov/react-native-sms-user-consent": "1.3.0",
"@react-native-async-storage/async-storage": "^1.23.1",
"@react-native-cookies/cookies": "^6.2.1",
"@react-native-ml-kit/barcode-scanning": "^2.0.0",
@@ -26,7 +27,6 @@
"react-native-gesture-handler": "~2.9.0",
"react-native-safe-area-context": "^4.4.1",
"react-native-screens": "~3.36.0",
"react-native-sms-retriever": "1.1.1",
"react-native-svg": "^14.1.0",
"react-native-svg-transformer": "^1.5.3",
"react-native-tcp-socket": "^6.4.1",

View File

@@ -0,0 +1,13 @@
diff --git a/node_modules/@eabdullazyanov/react-native-sms-user-consent/android/src/main/java/com/akvelon/reactnativesmsuserconsent/ReactNativeSmsUserConsentModule.java b/node_modules/@eabdullazyanov/react-native-sms-user-consent/android/src/main/java/com/akvelon/reactnativesmsuserconsent/ReactNativeSmsUserConsentModule.java
index 0000000..1111111 100644
--- a/node_modules/@eabdullazyanov/react-native-sms-user-consent/android/src/main/java/com/akvelon/reactnativesmsuserconsent/ReactNativeSmsUserConsentModule.java
+++ b/node_modules/@eabdullazyanov/react-native-sms-user-consent/android/src/main/java/com/akvelon/reactnativesmsuserconsent/ReactNativeSmsUserConsentModule.java
@@ -49,7 +49,7 @@ public class ReactNativeSmsUserConsentModule extends ReactContextBaseJavaModule
SmsRetriever.getClient(getCurrentActivity()).startSmsUserConsent(null);
broadcastReceiver = new SmsBroadcastReceiver(getCurrentActivity(), this);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ if (Build.VERSION.SDK_INT >= 34) {
getCurrentActivity().registerReceiver(
broadcastReceiver,
new IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION),

View File

@@ -1,69 +0,0 @@
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);
}

32
utils/smsUserConsent.ts Normal file
View File

@@ -0,0 +1,32 @@
import { Platform } from 'react-native';
import {
retrieveVerificationCode,
startSmsHandling,
} from '@eabdullazyanov/react-native-sms-user-consent';
/** 用户点允许后,从短信正文取数字(优先 preferLength 位,否则取第一段数字) */
export function digitsFromSms(message: string, preferLength?: number): string {
if (preferLength) {
const exact = retrieveVerificationCode(message, preferLength);
if (exact) return exact;
}
const m = message.match(/\d+/);
return m ? m[0] : '';
}
/**
* SMS User Consent系统弹窗用户同意后才回调无需 READ_SMS / app hash。
*/
export function startSmsUserConsentListener(
preferLength: number,
onDigits: (digits: string) => void,
): () => void {
if (Platform.OS !== 'android') return () => {};
return startSmsHandling((event) => {
const sms = event?.sms ?? '';
if (!sms) return;
const digits = digitsFromSms(sms, preferLength);
if (digits) onDigits(digits);
});
}