自动读取短信
This commit is contained in:
@@ -6,9 +6,7 @@ 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';
|
||||||
|
|
||||||
@@ -59,29 +57,6 @@ 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;
|
||||||
@@ -123,7 +98,6 @@ 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>
|
||||||
@@ -160,6 +134,7 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
|
|||||||
{state.otpSent && (
|
{state.otpSent && (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.hint}>OTP sent to {state.mobile}</Text>
|
<Text style={styles.hint}>OTP sent to {state.mobile}</Text>
|
||||||
|
<Text style={styles.consentHint}>收到短信后点「允许」可填入验证码</Text>
|
||||||
<Text style={styles.label}>Verification code</Text>
|
<Text style={styles.label}>Verification code</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.input, !!state.errorMessage && styles.inputError]}
|
style={[styles.input, !!state.errorMessage && styles.inputError]}
|
||||||
@@ -232,7 +207,6 @@ 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}
|
||||||
@@ -251,6 +225,7 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
|
|||||||
{showOtp && (
|
{showOtp && (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.hint}>OTP sent to {state.mobile}</Text>
|
<Text style={styles.hint}>OTP sent to {state.mobile}</Text>
|
||||||
|
<Text style={styles.consentHint}>收到短信后点「允许」可填入验证码</Text>
|
||||||
{!!state.errorMessage && (
|
{!!state.errorMessage && (
|
||||||
<Text style={styles.errorText}>{state.errorMessage}</Text>
|
<Text style={styles.errorText}>{state.errorMessage}</Text>
|
||||||
)}
|
)}
|
||||||
@@ -338,6 +313,13 @@ const styles = StyleSheet.create({
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
marginBottom: 14,
|
marginBottom: 14,
|
||||||
},
|
},
|
||||||
|
consentHint: {
|
||||||
|
fontSize: 11,
|
||||||
|
color: '#888',
|
||||||
|
textAlign: 'center',
|
||||||
|
marginTop: -8,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
input: {
|
input: {
|
||||||
borderWidth: 1.5,
|
borderWidth: 1.5,
|
||||||
borderColor: '#e0e0e0',
|
borderColor: '#e0e0e0',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
import { WalletType } from 'rnwalletman';
|
import { WalletType } from 'rnwalletman';
|
||||||
import { startOtpSmsListener } from '../utils/smsRetriever';
|
import { startSmsUserConsentListener } from '../utils/smsUserConsent';
|
||||||
|
|
||||||
export interface OTPBindCallbacks {
|
export interface OTPBindCallbacks {
|
||||||
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
|
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
|
||||||
@@ -104,22 +104,16 @@ export function useOTPBind(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
(async () => {
|
|
||||||
smsStopRef.current?.();
|
smsStopRef.current?.();
|
||||||
const stop = await startOtpSmsListener(otpLength, (code) => {
|
const stop = startSmsUserConsentListener(otpLength, (digits) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setOtp(code);
|
setOtp(digits);
|
||||||
log('OTP from SMS Retriever:', code);
|
log('SMS User Consent:', digits);
|
||||||
});
|
});
|
||||||
if (cancelled) {
|
|
||||||
stop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
smsStopRef.current = stop;
|
smsStopRef.current = stop;
|
||||||
})();
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
smsStopRef.current?.();
|
stop();
|
||||||
smsStopRef.current = null;
|
smsStopRef.current = null;
|
||||||
};
|
};
|
||||||
}, [shouldListenSms, smsListenKey, otpLength]);
|
}, [shouldListenSms, smsListenKey, otpLength]);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"postinstall": "patch-package"
|
"postinstall": "patch-package"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@eabdullazyanov/react-native-sms-user-consent": "1.3.0",
|
||||||
"@react-native-async-storage/async-storage": "^1.23.1",
|
"@react-native-async-storage/async-storage": "^1.23.1",
|
||||||
"@react-native-cookies/cookies": "^6.2.1",
|
"@react-native-cookies/cookies": "^6.2.1",
|
||||||
"@react-native-ml-kit/barcode-scanning": "^2.0.0",
|
"@react-native-ml-kit/barcode-scanning": "^2.0.0",
|
||||||
@@ -26,7 +27,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -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