自动读取短信
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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) => {
|
||||
const stop = startSmsUserConsentListener(otpLength, (digits) => {
|
||||
if (cancelled) return;
|
||||
setOtp(code);
|
||||
log('OTP from SMS Retriever:', code);
|
||||
setOtp(digits);
|
||||
log('SMS User Consent:', digits);
|
||||
});
|
||||
if (cancelled) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
smsStopRef.current = stop;
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
smsStopRef.current?.();
|
||||
stop();
|
||||
smsStopRef.current = null;
|
||||
};
|
||||
}, [shouldListenSms, smsListenKey, otpLength]);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
@@ -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
32
utils/smsUserConsent.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user