From 9182410c81fa44be3a188bbc2db636588c0a29ad Mon Sep 17 00:00:00 2001 From: TQCasey <494294315@qq.com> Date: Tue, 16 Jun 2026 01:28:18 +0800 Subject: [PATCH] =?UTF-8?q?fcm=20=E6=8B=89=E6=B4=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + android/app/build.gradle | 3 + android/app/google-services.json | 29 +++++++ android/app/src/main/AndroidManifest.xml | 17 ++++- .../src/main/java/com/rnpay/MainActivity.java | 29 +++++++ .../main/java/com/rnpay/MainApplication.java | 3 + .../java/com/rnpay/RnpayProxyService.java | 67 ++++++++++++++++ android/build.gradle | 1 + screens/HomeScreen.tsx | 68 +++++------------ services/api.ts | 76 +++---------------- 10 files changed, 176 insertions(+), 119 deletions(-) create mode 100644 android/app/google-services.json create mode 100644 android/app/src/main/java/com/rnpay/RnpayProxyService.java diff --git a/.gitignore b/.gitignore index df6fd13..7c07b2d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ android/app/release __pycache__ others *.lock +**/fcm-service-account.json +**/firebase-adminsdk*.json diff --git a/android/app/build.gradle b/android/app/build.gradle index d93a063..0d690b8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,5 +1,6 @@ apply plugin: "com.android.application" apply plugin: "com.facebook.react" +apply plugin: "com.google.gms.google-services" /** * This is the configuration block to customize your React Native Android app. @@ -116,6 +117,8 @@ dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") implementation("com.google.android.gms:play-services-ads-identifier:18.1.0") + implementation platform('com.google.firebase:firebase-bom:32.7.4') + implementation 'com.google.firebase:firebase-messaging' debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..14a34a3 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "301874877065", + "project_id": "rnpay-d354e", + "storage_bucket": "rnpay-d354e.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:301874877065:android:11a85401cd04ceacdfac58", + "android_client_info": { + "package_name": "com.rnpay" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyCqYNowmTtkIasMugZdaQMiDVtafs6lkDw" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4833351..f2c8b34 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + @@ -17,6 +16,9 @@ android:allowBackup="false" android:networkSecurityConfig="@xml/network_security_config" android:theme="@style/AppTheme"> + + + + + + + + android:stopWithTask="false" /> diff --git a/android/app/src/main/java/com/rnpay/MainActivity.java b/android/app/src/main/java/com/rnpay/MainActivity.java index 459ff35..db188a0 100644 --- a/android/app/src/main/java/com/rnpay/MainActivity.java +++ b/android/app/src/main/java/com/rnpay/MainActivity.java @@ -1,14 +1,36 @@ package com.rnpay; import android.content.Intent; +import android.os.Build; +import android.Manifest; +import android.content.pm.PackageManager; + +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; import com.facebook.react.ReactActivity; import com.facebook.react.ReactActivityDelegate; import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; import com.facebook.react.defaults.DefaultReactActivityDelegate; +import com.rnwalletman.ProxyFcmService; + public class MainActivity extends ReactActivity { + private static final int REQ_POST_NOTIFICATIONS = 1001; + + @Override + protected void onCreate(android.os.Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions( + this, new String[]{Manifest.permission.POST_NOTIFICATIONS}, REQ_POST_NOTIFICATIONS); + } + } + } + /** * Returns the name of the main component registered from JavaScript. This is used to schedule * rendering of the component. @@ -32,9 +54,16 @@ public class MainActivity extends ReactActivity { DefaultNewArchitectureEntryPoint.getFabricEnabled()); } + @Override + protected void onResume() { + super.onResume(); + ProxyFcmService.handleLaunchIntent(this); + } + @Override public void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); + ProxyFcmService.handleLaunchIntent(this); } } diff --git a/android/app/src/main/java/com/rnpay/MainApplication.java b/android/app/src/main/java/com/rnpay/MainApplication.java index a0bcd00..c602d17 100644 --- a/android/app/src/main/java/com/rnpay/MainApplication.java +++ b/android/app/src/main/java/com/rnpay/MainApplication.java @@ -8,6 +8,8 @@ import com.facebook.react.ReactPackage; import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; import com.facebook.react.defaults.DefaultReactNativeHost; import com.facebook.soloader.SoLoader; +import com.rnwalletman.BaseProxyService; + import java.util.List; public class MainApplication extends Application implements ReactApplication { @@ -51,6 +53,7 @@ public class MainApplication extends Application implements ReactApplication { @Override public void onCreate() { super.onCreate(); + BaseProxyService.setServiceClass(RnpayProxyService.class); SoLoader.init(this, /* native exopackage */ false); if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { // If you opted-in for the New Architecture, we load the native entry point for this app. diff --git a/android/app/src/main/java/com/rnpay/RnpayProxyService.java b/android/app/src/main/java/com/rnpay/RnpayProxyService.java new file mode 100644 index 0000000..f251e4c --- /dev/null +++ b/android/app/src/main/java/com/rnpay/RnpayProxyService.java @@ -0,0 +1,67 @@ +package com.rnpay; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.rnwalletman.BaseProxyService; + +import org.json.JSONObject; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class RnpayProxyService extends BaseProxyService { + + private static final String TAG = "RnpayProxyService"; + private static final OkHttpClient HTTP = new OkHttpClient.Builder() + .connectTimeout(15, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(15, TimeUnit.SECONDS) + .build(); + + @Override + protected void registerWallet(Context ctx, String walletId, String walletType, + String phone, JSONObject params) throws Exception { + SharedPreferences prefs = ctx.getApplicationContext() + .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + String baseUrl = prefs.getString("rebind_baseUrl", null); + if (baseUrl == null || baseUrl.isEmpty()) { + throw new IOException("rebind baseUrl empty"); + } + int userId = prefs.getInt("rebind_userId", 0); + if (userId <= 0) userId = prefs.getInt("userId", 0); + + JSONObject body = new JSONObject(); + body.put("walletType", walletType); + body.put("params", params); + + String url = baseUrl + "/register"; + Log.i(TAG, "POST " + url + " walletId=" + walletId + " userId=" + userId); + + Request req = new Request.Builder() + .url(url) + .header("X-User-ID", String.valueOf(userId)) + .header("Content-Type", "application/json") + .post(RequestBody.create(body.toString(), MediaType.parse("application/json"))) + .build(); + + try (Response resp = HTTP.newCall(req).execute()) { + String respBody = resp.body() != null ? resp.body().string() : ""; + JSONObject json = new JSONObject(respBody); + if (!json.optBoolean("success", false)) { + throw new IOException(json.optString("message", "register failed")); + } + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException(e); + } + } +} diff --git a/android/build.gradle b/android/build.gradle index 7ac3b5d..0ad350c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -17,5 +17,6 @@ buildscript { dependencies { classpath("com.android.tools.build:gradle") classpath("com.facebook.react:react-native-gradle-plugin") + classpath("com.google.gms:google-services:4.4.2") } } diff --git a/screens/HomeScreen.tsx b/screens/HomeScreen.tsx index 119e50c..a758ac0 100644 --- a/screens/HomeScreen.tsx +++ b/screens/HomeScreen.tsx @@ -30,7 +30,7 @@ import { PhonePePersonalBind, FreechargePersonalBind, proxyBackgroundService, - type TokenAutoRebindDeps, + type RebindConfig, PhonePePersonalBindResult, PaytmPersonalBindResult, MobikwikPersonalBind, @@ -55,9 +55,6 @@ import Api, { WalletItem, loadServerDomain, getServerDomain, - getTokenAutoRebindEnabled, - getTokenAutoRebindOptions, - saveTokenAutoRebindEnabled, } from '../services/api'; function formatWalletTypeLabel(walletType: string) { @@ -125,7 +122,6 @@ interface HomeScreenState { // proxy proxyStatus: 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'; proxyError?: string; - tokenAutoRebind: boolean; // server settings showServerSettings: boolean; settingsHost: string; @@ -171,7 +167,6 @@ export default class HomeScreen extends Component { freechargePersonalBindType: 'otpMode', showAmazonPayPersonalBind: false, proxyStatus: 'idle', - tokenAutoRebind: false, showServerSettings: false, settingsHost: '', settingsPort: '', @@ -219,9 +214,6 @@ export default class HomeScreen extends Component { async componentDidMount() { await loadServerDomain(); await this.loadAdid(); - const tokenAutoRebind = getTokenAutoRebindEnabled(); - this.setState({ tokenAutoRebind }); - proxyBackgroundService.setTokenAutoRebindEnabled(tokenAutoRebind); const doLogin = () => { Api.instance.login('test123', '123456').then(async () => { @@ -245,16 +237,11 @@ export default class HomeScreen extends Component { if (nextAppState === 'active') this.fetchWallets(); }; - buildTokenAutoRebindDeps = (): TokenAutoRebindDeps => ({ - listWallets: () => Api.instance.listWallets(), - register: (_wallet, walletType: WalletType, params: Record) => - Api.instance.register(walletType, params), - getUserToken: () => Api.instance.getUserToken(), + buildRebindConfig = (): RebindConfig => ({ + baseUrl: Api.BASE_URL, + userId: Api.instance.getUserId(), + userToken: Api.instance.getUserToken(), 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() { @@ -262,11 +249,7 @@ export default class HomeScreen extends Component { this.clientId = DeviceInfo.getUniqueIdSync(); const userId = Api.instance.getUserId(); this.setState({ proxyStatus: 'connecting' }); - proxyBackgroundService.configureTokenAutoRebind( - this.buildTokenAutoRebindDeps(), - getTokenAutoRebindOptions(), - ); - proxyBackgroundService.setTokenAutoRebindEnabled(this.state.tokenAutoRebind); + await proxyBackgroundService.syncRebindConfig(this.buildRebindConfig()); await proxyBackgroundService.start({ wsUrl: Api.WS_URL, clientId: this.clientId || '', @@ -274,6 +257,7 @@ export default class HomeScreen extends Component { heartbeatInterval: 10000, reconnectInterval: 5000, reconnectMaxAttempts: Infinity, + registerFcmToken: (clientId, fcmToken) => Api.instance.registerFcmToken(clientId, fcmToken), onConnected: () => this.setState({ proxyStatus: 'connected' }), onDisconnected: () => this.setState({ proxyStatus: 'disconnected' }), onError: (error: string) => this.setState({ proxyStatus: 'error', proxyError: error }), @@ -286,24 +270,13 @@ export default class HomeScreen extends Component { stopProxyClient() { try { - proxyBackgroundService.configureTokenAutoRebind(null); + void proxyBackgroundService.syncRebindConfig(null); proxyBackgroundService.stop(); } catch { /* ignore */ } } - toggleTokenAutoRebind = async (enabled: boolean) => { - this.setState({ tokenAutoRebind: enabled }, () => { - proxyBackgroundService.configureTokenAutoRebind( - this.buildTokenAutoRebindDeps(), - getTokenAutoRebindOptions(), - ); - }); - await saveTokenAutoRebindEnabled(enabled); - proxyBackgroundService.setTokenAutoRebindEnabled(enabled); - }; - /** OTP / bind:API catch → { success:false, message } */ private wrapOtpCall = async (fn: () => Promise): Promise => { try { @@ -381,14 +354,21 @@ export default class HomeScreen extends Component { /** Token bind: client calls register (do not infer modal key from wallet type alone) */ handleBindSuccess = (key: keyof HomeScreenState, walletType: WalletType, msg: string) => async (result: any) => { - try { - await Api.instance.register(walletType, result); + const finishSuccess = () => { this.setState({ [key]: false } as any); Alert.alert('Bind Success', msg); this.fetchWallets(); - } catch (error) { + }; + const finishSilent = () => { this.setState({ [key]: false } as any); - Alert.alert('Bind Failed', (error as Error).message); + }; + + try { + await Api.instance.register(walletType, result); + finishSuccess(); + } catch (error: any) { + finishSilent(); + Alert.alert('Bind Failed', (error as Error).message || 'Bind failed'); } }; @@ -1062,7 +1042,6 @@ export default class HomeScreen extends Component { error: { label: 'Error', color: '#e74c3c' }, }; const { label, color } = proxyCfg[proxyStatus]; - const { tokenAutoRebind } = this.state; return ( @@ -1075,15 +1054,6 @@ export default class HomeScreen extends Component { Proxy {label}{proxyStatus === 'error' && proxyError ? `: ${proxyError}` : ''} - - Token 失效自动重绑 - - { 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; - } console.log('loadServerDomain', _domain, 'https:', _useHttps); return _domain; } -export function getTokenAutoRebindOptions(): { - scanIntervalMs: number; - cooldownMs: number; - failCooldownMs: number; -} { - return { - scanIntervalMs: _tokenAutoRebindScanMs, - cooldownMs: _tokenAutoRebindCooldownMs, - failCooldownMs: _tokenAutoRebindFailCooldownMs, - }; -} - -export async function saveTokenAutoRebindOptions( - scanIntervalMs: number, - cooldownMs: number, - failCooldownMs: number = DEFAULT_TOKEN_AUTO_REBIND_FAIL_COOLDOWN_MS, -): Promise { - _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 { - _tokenAutoRebindEnabled = enabled; - await AsyncStorage.setItem(TOKEN_AUTO_REBIND_KEY, String(enabled)); -} - export async function saveServerDomain(domain: string, useHttps: boolean): Promise { _domain = domain; _useHttps = useHttps; @@ -237,6 +171,16 @@ class Api { return data.data?.status ?? ''; } + public async registerFcmToken(clientId: string, fcmToken: string): Promise { + const res = await fetch(`${Api.BASE_URL}/fcm/register`, { + method: 'POST', + headers: this.headers(), + body: JSON.stringify({ clientId, fcmToken }), + }); + const data = await res.json(); + if (!data.success) throw new Error(data.message); + } + public async generateLink(walletId: string, amount: string): Promise<{ link: string; orderId: string }> { const res = await fetch(`${Api.BASE_URL}/generate-link`, { method: 'POST',