ios app
This commit is contained in:
6
argon-pro-react-native/AuthKey_HSF578B626.p8
Normal file
6
argon-pro-react-native/AuthKey_HSF578B626.p8
Normal file
@@ -0,0 +1,6 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgLshiAgsJoiJegXNC
|
||||
55cF2MHBnQCi2AaObrf/qgEavcmgCgYIKoZIzj0DAQehRANCAAQoIgTclBUyCDU2
|
||||
gFaphqK1I4n1VAkEad144GMKxrdjwfAXbOenkDkUis/6LBEMoOI8tBTcwP1qlY7s
|
||||
V7zdIhb4
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -23,15 +23,29 @@
|
||||
],
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "com.valuefrontier.meagent"
|
||||
"bundleIdentifier": "com.valuefrontier.meagent",
|
||||
"infoPlist": {
|
||||
"UIBackgroundModes": ["remote-notification"]
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
"package": "com.valuefrontier.meagent",
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/logo.jpg",
|
||||
"backgroundColor": "#000000"
|
||||
}
|
||||
},
|
||||
"googleServicesFile": "./google-services.json"
|
||||
},
|
||||
"plugins": [
|
||||
[
|
||||
"expo-notifications",
|
||||
{
|
||||
"icon": "./assets/logo.jpg",
|
||||
"color": "#D4AF37",
|
||||
"sounds": []
|
||||
}
|
||||
]
|
||||
],
|
||||
"description": "价值前沿 - 智能投资助手"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ import { MarketHot, SectorDetail, EventCalendar, StockDetail, TodayStats } from
|
||||
// 认证页面
|
||||
import { LoginScreen } from "../src/screens/Auth";
|
||||
|
||||
// 推送通知处理
|
||||
import PushNotificationHandler from "../src/components/PushNotificationHandler";
|
||||
|
||||
const { width } = Dimensions.get("screen");
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
@@ -643,28 +646,30 @@ function AppStack(props) {
|
||||
|
||||
export default function OnboardingStack(props) {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{
|
||||
mode: "card",
|
||||
headerShown: false,
|
||||
}}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="Onboarding"
|
||||
component={Pro}
|
||||
option={{
|
||||
headerTransparent: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen name="App" component={AppStack} />
|
||||
<Stack.Screen
|
||||
name="Login"
|
||||
component={LoginScreen}
|
||||
options={{
|
||||
presentation: "modal",
|
||||
<PushNotificationHandler>
|
||||
<Stack.Navigator
|
||||
screenOptions={{
|
||||
mode: "card",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
>
|
||||
<Stack.Screen
|
||||
name="Onboarding"
|
||||
component={Pro}
|
||||
option={{
|
||||
headerTransparent: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen name="App" component={AppStack} />
|
||||
<Stack.Screen
|
||||
name="Login"
|
||||
component={LoginScreen}
|
||||
options={{
|
||||
presentation: "modal",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</PushNotificationHandler>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,9 +25,12 @@
|
||||
"expo": "~51.0.28",
|
||||
"expo-asset": "~10.0.10",
|
||||
"expo-blur": "~13.0.3",
|
||||
"expo-constants": "~16.0.2",
|
||||
"expo-device": "~6.0.2",
|
||||
"expo-font": "~12.0.10",
|
||||
"expo-linear-gradient": "~13.0.2",
|
||||
"expo-modules-core": "~1.12.24",
|
||||
"expo-notifications": "~0.28.19",
|
||||
"expo-splash-screen": "~0.27.5",
|
||||
"galio-framework": "^0.8.0",
|
||||
"native-base": "^3.4.28",
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 推送通知处理器
|
||||
* 在 Navigation 内部使用,处理推送通知
|
||||
*/
|
||||
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { pushService } from '../services/pushService';
|
||||
|
||||
export function PushNotificationHandler({ children }) {
|
||||
const navigation = useNavigation();
|
||||
|
||||
// 处理收到的通知(App 在前台时)
|
||||
const handleNotificationReceived = useCallback((notification) => {
|
||||
const data = notification.request.content.data;
|
||||
console.log('[Push] 前台收到通知:', data);
|
||||
// 可以触发事件列表刷新等
|
||||
}, []);
|
||||
|
||||
// 处理用户点击通知
|
||||
const handleNotificationResponse = useCallback((response) => {
|
||||
const data = response.notification.request.content.data;
|
||||
console.log('[Push] 用户点击通知:', data);
|
||||
|
||||
// 导航到事件详情
|
||||
if (data?.event_id) {
|
||||
// 先导航到事件中心,再进入详情
|
||||
navigation.navigate('App', {
|
||||
screen: 'EventsDrawer',
|
||||
params: {
|
||||
screen: 'EventDetail',
|
||||
params: {
|
||||
eventId: data.event_id,
|
||||
title: data.title || '事件详情',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [navigation]);
|
||||
|
||||
// 初始化推送
|
||||
useEffect(() => {
|
||||
const initPush = async () => {
|
||||
try {
|
||||
const token = await pushService.initialize(
|
||||
handleNotificationReceived,
|
||||
handleNotificationResponse
|
||||
);
|
||||
if (token) {
|
||||
console.log('[Push] 初始化成功,Token 已注册');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Push] 初始化失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
initPush();
|
||||
|
||||
// 清理:不在组件卸载时取消注册,保持后台推送能力
|
||||
return () => {
|
||||
// 如果需要完全退出时取消注册,在 logout 处调用
|
||||
};
|
||||
}, [handleNotificationReceived, handleNotificationResponse]);
|
||||
|
||||
// App 激活时清除角标
|
||||
useEffect(() => {
|
||||
pushService.clearBadge();
|
||||
}, []);
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
export default PushNotificationHandler;
|
||||
84
argon-pro-react-native/src/hooks/usePushNotifications.js
Normal file
84
argon-pro-react-native/src/hooks/usePushNotifications.js
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 推送通知 Hook
|
||||
* 在 App 中初始化和处理推送通知
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { pushService } from '../services/pushService';
|
||||
|
||||
/**
|
||||
* 推送通知 Hook
|
||||
* @param {object} options
|
||||
* @param {boolean} options.enabled - 是否启用推送
|
||||
* @returns {object} { deviceToken, clearBadge }
|
||||
*/
|
||||
export function usePushNotifications(options = { enabled: true }) {
|
||||
const navigation = useNavigation();
|
||||
const deviceTokenRef = useRef(null);
|
||||
|
||||
// 处理收到的通知(App 在前台时)
|
||||
const handleNotificationReceived = useCallback((notification) => {
|
||||
const data = notification.request.content.data;
|
||||
console.log('[Push Hook] 前台收到通知:', data);
|
||||
|
||||
// 可以在这里触发列表刷新等操作
|
||||
// 例如:dispatch(fetchEvents({ refresh: true }));
|
||||
}, []);
|
||||
|
||||
// 处理用户点击通知
|
||||
const handleNotificationResponse = useCallback((response) => {
|
||||
const data = response.notification.request.content.data;
|
||||
console.log('[Push Hook] 用户点击通知:', data);
|
||||
|
||||
// 根据通知数据导航到对应页面
|
||||
if (data?.event_id) {
|
||||
// 导航到事件详情
|
||||
navigation.navigate('EventsDrawer', {
|
||||
screen: 'EventDetail',
|
||||
params: {
|
||||
eventId: data.event_id,
|
||||
title: data.title || '事件详情',
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [navigation]);
|
||||
|
||||
// 初始化推送服务
|
||||
useEffect(() => {
|
||||
if (!options.enabled) return;
|
||||
|
||||
const initPush = async () => {
|
||||
try {
|
||||
const token = await pushService.initialize(
|
||||
handleNotificationReceived,
|
||||
handleNotificationResponse
|
||||
);
|
||||
deviceTokenRef.current = token;
|
||||
console.log('[Push Hook] 推送初始化完成, token:', token ? '已获取' : '未获取');
|
||||
} catch (error) {
|
||||
console.error('[Push Hook] 推送初始化失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
initPush();
|
||||
|
||||
// 清理
|
||||
return () => {
|
||||
// 注意:不在这里 unregister,因为我们希望保持推送注册
|
||||
// 如果需要完全退出登录时取消注册,应该在 logout 时调用 pushService.unregister()
|
||||
};
|
||||
}, [options.enabled, handleNotificationReceived, handleNotificationResponse]);
|
||||
|
||||
// 清除角标
|
||||
const clearBadge = useCallback(() => {
|
||||
pushService.clearBadge();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
deviceToken: deviceTokenRef.current,
|
||||
clearBadge,
|
||||
};
|
||||
}
|
||||
|
||||
export default usePushNotifications;
|
||||
203
argon-pro-react-native/src/services/pushService.js
Normal file
203
argon-pro-react-native/src/services/pushService.js
Normal file
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* 推送通知服务
|
||||
* 处理 APNs 推送注册和通知处理
|
||||
*/
|
||||
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import * as Device from 'expo-device';
|
||||
import Constants from 'expo-constants';
|
||||
import { Platform } from 'react-native';
|
||||
import { apiRequest } from './api';
|
||||
|
||||
// 配置通知显示方式
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => ({
|
||||
shouldShowAlert: true,
|
||||
shouldPlaySound: true,
|
||||
shouldSetBadge: true,
|
||||
}),
|
||||
});
|
||||
|
||||
class PushService {
|
||||
constructor() {
|
||||
this.expoPushToken = null;
|
||||
this.devicePushToken = null;
|
||||
this.notificationListener = null;
|
||||
this.responseListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化推送服务
|
||||
* @param {function} onNotificationReceived - 收到通知时的回调
|
||||
* @param {function} onNotificationResponse - 用户点击通知时的回调
|
||||
*/
|
||||
async initialize(onNotificationReceived, onNotificationResponse) {
|
||||
// 注册推送
|
||||
await this.registerForPushNotifications();
|
||||
|
||||
// 监听前台收到的通知
|
||||
this.notificationListener = Notifications.addNotificationReceivedListener(
|
||||
(notification) => {
|
||||
console.log('[Push] 收到通知:', notification);
|
||||
if (onNotificationReceived) {
|
||||
onNotificationReceived(notification);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 监听用户点击通知
|
||||
this.responseListener = Notifications.addNotificationResponseReceivedListener(
|
||||
(response) => {
|
||||
console.log('[Push] 用户点击通知:', response);
|
||||
if (onNotificationResponse) {
|
||||
onNotificationResponse(response);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return this.devicePushToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册推送通知
|
||||
*/
|
||||
async registerForPushNotifications() {
|
||||
// 检查是否是真机
|
||||
if (!Device.isDevice) {
|
||||
console.log('[Push] 模拟器不支持推送通知');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
||||
let finalStatus = existingStatus;
|
||||
|
||||
// 如果没有权限,请求权限
|
||||
if (existingStatus !== 'granted') {
|
||||
const { status } = await Notifications.requestPermissionsAsync();
|
||||
finalStatus = status;
|
||||
}
|
||||
|
||||
if (finalStatus !== 'granted') {
|
||||
console.log('[Push] 推送通知权限被拒绝');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取原生 APNs/FCM token(用于直接 APNs 推送)
|
||||
const deviceToken = await Notifications.getDevicePushTokenAsync();
|
||||
this.devicePushToken = deviceToken.data;
|
||||
console.log('[Push] Device Token:', this.devicePushToken);
|
||||
|
||||
// 发送 token 到后端
|
||||
await this.sendTokenToServer(this.devicePushToken);
|
||||
|
||||
return this.devicePushToken;
|
||||
} catch (error) {
|
||||
console.error('[Push] 获取推送 Token 失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 device token 到后端保存
|
||||
*/
|
||||
async sendTokenToServer(token) {
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const response = await apiRequest('/api/users/device-token', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
device_token: token,
|
||||
platform: Platform.OS,
|
||||
app_version: Constants.expoConfig?.version || '1.0.0',
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
console.log('[Push] Token 已注册到服务器');
|
||||
} else {
|
||||
console.warn('[Push] Token 注册失败:', response.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Push] 发送 Token 到服务器失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新推送订阅设置
|
||||
* @param {object} settings - 订阅设置
|
||||
* @param {boolean} settings.all_events - 订阅所有事件
|
||||
* @param {boolean} settings.important_only - 只订阅重要事件 (S/A级)
|
||||
* @param {string[]} settings.event_types - 订阅的事件类型
|
||||
*/
|
||||
async updateSubscription(settings) {
|
||||
if (!this.devicePushToken) {
|
||||
console.warn('[Push] 无 device token,无法更新订阅');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiRequest('/api/users/push-subscription', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
device_token: this.devicePushToken,
|
||||
...settings,
|
||||
}),
|
||||
});
|
||||
|
||||
return response.success;
|
||||
} catch (error) {
|
||||
console.error('[Push] 更新订阅失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消注册推送
|
||||
*/
|
||||
async unregister() {
|
||||
if (this.devicePushToken) {
|
||||
try {
|
||||
await apiRequest('/api/users/device-token', {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({
|
||||
device_token: this.devicePushToken,
|
||||
}),
|
||||
});
|
||||
console.log('[Push] Token 已从服务器移除');
|
||||
} catch (error) {
|
||||
console.error('[Push] 移除 Token 失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理监听器
|
||||
if (this.notificationListener) {
|
||||
Notifications.removeNotificationSubscription(this.notificationListener);
|
||||
}
|
||||
if (this.responseListener) {
|
||||
Notifications.removeNotificationSubscription(this.responseListener);
|
||||
}
|
||||
|
||||
this.devicePushToken = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除通知角标
|
||||
*/
|
||||
async clearBadge() {
|
||||
await Notifications.setBadgeCountAsync(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前的 device token
|
||||
*/
|
||||
getDeviceToken() {
|
||||
return this.devicePushToken;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const pushService = new PushService();
|
||||
export default pushService;
|
||||
Reference in New Issue
Block a user