feat(review): 集成iOS应用内评分功能
- 新增iOS原生模块AppStoreReviewManager,封装StoreKit评分请求 - 实现appStoreReviewService服务层,管理评分请求时间间隔(14天) - 在关键用户操作后触发评分请求:完成挑战、记录服药、记录体重、记录饮水 - 优化通知设置页面UI,改进设置项布局和视觉层次 - 调整用药卡片样式,优化状态显示和文字大小 - 新增配置检查脚本check-app-review-setup.sh - 修改喝水提醒默认状态为关闭 评分请求策略: - 仅iOS 14.0+支持 - 自动控制请求频率,避免过度打扰用户 - 延迟1秒执行,不阻塞主业务流程 - 所有评分请求均做错误处理,确保不影响核心功能
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||
import { palette } from '@/constants/Colors';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { useNotifications } from '@/hooks/useNotifications';
|
||||
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
||||
import {
|
||||
getMedicationReminderEnabled,
|
||||
getMoodReminderEnabled,
|
||||
@@ -12,18 +14,15 @@ import {
|
||||
setNutritionReminderEnabled
|
||||
} from '@/utils/userPreferences';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { router, useFocusEffect } from 'expo-router';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Alert, Linking, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Alert, Linking, ScrollView, StatusBar, StyleSheet, Switch, Text, View } from 'react-native';
|
||||
|
||||
export default function NotificationSettingsScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const safeAreaTop = useSafeAreaTop(60);
|
||||
const { t } = useI18n();
|
||||
const { requestPermission, sendNotification } = useNotifications();
|
||||
const isLgAvailable = isLiquidGlassAvailable();
|
||||
|
||||
// 通知设置状态
|
||||
const [notificationEnabled, setNotificationEnabledState] = useState(false);
|
||||
@@ -174,57 +173,41 @@ export default function NotificationSettingsScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
// 返回按钮
|
||||
const BackButton = () => (
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
style={styles.backButton}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{isLgAvailable ? (
|
||||
<GlassView
|
||||
style={styles.glassButton}
|
||||
glassEffectStyle="clear"
|
||||
tintColor="rgba(255, 255, 255, 0.3)"
|
||||
isInteractive={true}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color="#333" />
|
||||
</GlassView>
|
||||
) : (
|
||||
<View style={[styles.glassButton, styles.fallbackButton]}>
|
||||
<Ionicons name="chevron-back" size={24} color="#333" />
|
||||
// 渲染设置项
|
||||
const renderSettingItem = (
|
||||
icon: keyof typeof Ionicons.glyphMap,
|
||||
title: string,
|
||||
description: string,
|
||||
value: boolean,
|
||||
onValueChange: (value: boolean) => void,
|
||||
disabled: boolean = false,
|
||||
showSeparator: boolean = true
|
||||
) => (
|
||||
<View>
|
||||
<View style={styles.settingItem}>
|
||||
<View style={styles.itemInfo}>
|
||||
<View style={[styles.iconContainer, disabled && styles.iconContainerDisabled]}>
|
||||
<Ionicons name={icon} size={24} color={disabled ? '#C7C7CC' : '#9370DB'} />
|
||||
</View>
|
||||
<View style={styles.textContainer}>
|
||||
<Text style={[styles.itemTitle, disabled && styles.itemTitleDisabled]}>{title}</Text>
|
||||
<Text style={styles.itemDescription} numberOfLines={2}>{description}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Switch
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
disabled={disabled}
|
||||
trackColor={{ false: '#E5E5E5', true: '#9370DB' }}
|
||||
thumbColor="#FFFFFF"
|
||||
style={styles.switch}
|
||||
/>
|
||||
</View>
|
||||
{showSeparator && (
|
||||
<View style={styles.separatorContainer}>
|
||||
<View style={styles.separator} />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
// 开关项组件
|
||||
const SwitchItem = ({
|
||||
title,
|
||||
description,
|
||||
value,
|
||||
onValueChange,
|
||||
disabled = false
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
value: boolean;
|
||||
onValueChange: (value: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}) => (
|
||||
<View style={styles.switchItem}>
|
||||
<View style={styles.switchItemLeft}>
|
||||
<Text style={styles.switchItemTitle}>{title}</Text>
|
||||
<Text style={styles.switchItemDescription}>{description}</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
disabled={disabled}
|
||||
trackColor={{ false: '#E5E5E5', true: '#9370DB' }}
|
||||
thumbColor="#FFFFFF"
|
||||
style={styles.switch}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -233,10 +216,10 @@ export default function NotificationSettingsScreen() {
|
||||
<View style={styles.container}>
|
||||
<StatusBar barStyle="dark-content" backgroundColor="transparent" translucent />
|
||||
<LinearGradient
|
||||
colors={['#f5e5fbff', '#e5fcfeff', '#eefdffff', '#e6f6fcff']}
|
||||
colors={[palette.purple[100], '#F5F5F5']}
|
||||
start={{ x: 1, y: 0 }}
|
||||
end={{ x: 0.3, y: 0.4 }}
|
||||
style={styles.gradientBackground}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
/>
|
||||
<View style={styles.loadingContainer}>
|
||||
<Text style={styles.loadingText}>{t('notificationSettings.loading')}</Text>
|
||||
@@ -249,97 +232,82 @@ export default function NotificationSettingsScreen() {
|
||||
<View style={styles.container}>
|
||||
<StatusBar barStyle="dark-content" backgroundColor="transparent" translucent />
|
||||
|
||||
{/* 背景渐变 */}
|
||||
<LinearGradient
|
||||
colors={['#f5e5fbff', '#e5fcfeff', '#eefdffff', '#e6f6fcff']}
|
||||
colors={[palette.purple[100], '#F5F5F5']}
|
||||
start={{ x: 1, y: 0 }}
|
||||
end={{ x: 0.3, y: 0.4 }}
|
||||
style={styles.gradientBackground}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
/>
|
||||
|
||||
{/* 装饰性圆圈 */}
|
||||
<View style={styles.decorativeCircle1} />
|
||||
<View style={styles.decorativeCircle2} />
|
||||
<HeaderBar
|
||||
title={t('notificationSettings.title')}
|
||||
onBack={() => router.back()}
|
||||
/>
|
||||
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={{
|
||||
paddingTop: insets.top + 20,
|
||||
paddingBottom: insets.bottom + 20,
|
||||
paddingHorizontal: 16,
|
||||
}}
|
||||
contentContainerStyle={[
|
||||
styles.scrollContent,
|
||||
{ paddingTop: safeAreaTop }
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<View style={styles.header}>
|
||||
<BackButton />
|
||||
<ThemedText style={styles.title}>{t('notificationSettings.title')}</ThemedText>
|
||||
</View>
|
||||
|
||||
{/* 通知设置部分 */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>{t('notificationSettings.sections.notifications')}</Text>
|
||||
<View style={styles.card}>
|
||||
<SwitchItem
|
||||
title={t('notificationSettings.items.pushNotifications.title')}
|
||||
description={t('notificationSettings.items.pushNotifications.description')}
|
||||
value={notificationEnabled}
|
||||
onValueChange={handleNotificationToggle}
|
||||
/>
|
||||
{/* 顶部说明卡片 */}
|
||||
<View style={styles.headerSection}>
|
||||
<Text style={styles.subtitle}>{t('notificationSettings.sections.description')}</Text>
|
||||
<View style={styles.descriptionCard}>
|
||||
<View style={styles.hintRow}>
|
||||
<Ionicons name="information-circle-outline" size={20} color="#9370DB" />
|
||||
<Text style={styles.descriptionText}>
|
||||
{t('notificationSettings.description.text')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 药品提醒部分 */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>{t('notificationSettings.sections.medicationReminder')}</Text>
|
||||
<View style={styles.card}>
|
||||
<SwitchItem
|
||||
title={t('notificationSettings.items.medicationReminder.title')}
|
||||
description={t('notificationSettings.items.medicationReminder.description')}
|
||||
value={medicationReminderEnabled}
|
||||
onValueChange={handleMedicationReminderToggle}
|
||||
disabled={!notificationEnabled}
|
||||
/>
|
||||
</View>
|
||||
{/* 设置项列表 */}
|
||||
<View style={styles.sectionContainer}>
|
||||
{renderSettingItem(
|
||||
'notifications-outline',
|
||||
t('notificationSettings.items.pushNotifications.title'),
|
||||
t('notificationSettings.items.pushNotifications.description'),
|
||||
notificationEnabled,
|
||||
handleNotificationToggle,
|
||||
false,
|
||||
true
|
||||
)}
|
||||
|
||||
{renderSettingItem(
|
||||
'medkit-outline',
|
||||
t('notificationSettings.items.medicationReminder.title'),
|
||||
t('notificationSettings.items.medicationReminder.description'),
|
||||
medicationReminderEnabled,
|
||||
handleMedicationReminderToggle,
|
||||
!notificationEnabled,
|
||||
true
|
||||
)}
|
||||
|
||||
{renderSettingItem(
|
||||
'restaurant-outline',
|
||||
t('notificationSettings.items.nutritionReminder.title'),
|
||||
t('notificationSettings.items.nutritionReminder.description'),
|
||||
nutritionReminderEnabled,
|
||||
handleNutritionReminderToggle,
|
||||
!notificationEnabled,
|
||||
true
|
||||
)}
|
||||
|
||||
{renderSettingItem(
|
||||
'happy-outline',
|
||||
t('notificationSettings.items.moodReminder.title'),
|
||||
t('notificationSettings.items.moodReminder.description'),
|
||||
moodReminderEnabled,
|
||||
handleMoodReminderToggle,
|
||||
!notificationEnabled,
|
||||
false
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 营养提醒部分 */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>{t('notificationSettings.sections.nutritionReminder')}</Text>
|
||||
<View style={styles.card}>
|
||||
<SwitchItem
|
||||
title={t('notificationSettings.items.nutritionReminder.title')}
|
||||
description={t('notificationSettings.items.nutritionReminder.description')}
|
||||
value={nutritionReminderEnabled}
|
||||
onValueChange={handleNutritionReminderToggle}
|
||||
disabled={!notificationEnabled}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 心情提醒部分 */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>{t('notificationSettings.sections.moodReminder')}</Text>
|
||||
<View style={styles.card}>
|
||||
<SwitchItem
|
||||
title={t('notificationSettings.items.moodReminder.title')}
|
||||
description={t('notificationSettings.items.moodReminder.description')}
|
||||
value={moodReminderEnabled}
|
||||
onValueChange={handleMoodReminderToggle}
|
||||
disabled={!notificationEnabled}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 说明部分 */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>{t('notificationSettings.sections.description')}</Text>
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.description}>
|
||||
{t('notificationSettings.description.text')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
@@ -348,37 +316,22 @@ export default function NotificationSettingsScreen() {
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
gradientBackground: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
decorativeCircle1: {
|
||||
position: 'absolute',
|
||||
top: 40,
|
||||
right: 20,
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
backgroundColor: '#0EA5E9',
|
||||
opacity: 0.1,
|
||||
},
|
||||
decorativeCircle2: {
|
||||
position: 'absolute',
|
||||
bottom: -15,
|
||||
left: -15,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#0EA5E9',
|
||||
opacity: 0.05,
|
||||
height: '60%',
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
@@ -388,82 +341,95 @@ const styles = StyleSheet.create({
|
||||
fontSize: 16,
|
||||
color: '#666',
|
||||
},
|
||||
header: {
|
||||
headerSection: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
color: '#6C757D',
|
||||
marginBottom: 12,
|
||||
marginLeft: 4,
|
||||
},
|
||||
descriptionCard: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.6)',
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
gap: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(147, 112, 219, 0.1)',
|
||||
},
|
||||
hintRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 24,
|
||||
gap: 8,
|
||||
},
|
||||
backButton: {
|
||||
marginRight: 16,
|
||||
descriptionText: {
|
||||
flex: 1,
|
||||
fontSize: 13,
|
||||
color: '#2C3E50',
|
||||
lineHeight: 18,
|
||||
},
|
||||
glassButton: {
|
||||
sectionContainer: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 20,
|
||||
marginBottom: 20,
|
||||
overflow: 'hidden',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.03,
|
||||
shadowRadius: 8,
|
||||
elevation: 2,
|
||||
},
|
||||
settingItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
itemInfo: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
fallbackButton: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
color: '#2C3E50',
|
||||
},
|
||||
section: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#2C3E50',
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: 'rgba(147, 112, 219, 0.05)',
|
||||
borderRadius: 12,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
switchItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 16,
|
||||
iconContainerDisabled: {
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
switchItemLeft: {
|
||||
textContainer: {
|
||||
flex: 1,
|
||||
marginRight: 16,
|
||||
marginRight: 8,
|
||||
},
|
||||
switchItemTitle: {
|
||||
itemTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
color: '#2C3E50',
|
||||
marginBottom: 4,
|
||||
},
|
||||
switchItemDescription: {
|
||||
fontSize: 14,
|
||||
itemTitleDisabled: {
|
||||
color: '#999',
|
||||
},
|
||||
itemDescription: {
|
||||
fontSize: 12,
|
||||
color: '#6C757D',
|
||||
lineHeight: 20,
|
||||
lineHeight: 16,
|
||||
},
|
||||
switch: {
|
||||
transform: [{ scaleX: 0.8 }, { scaleY: 0.8 }],
|
||||
transform: [{ scaleX: 0.9 }, { scaleY: 0.9 }],
|
||||
},
|
||||
description: {
|
||||
fontSize: 14,
|
||||
color: '#6C757D',
|
||||
lineHeight: 22,
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 16,
|
||||
separatorContainer: {
|
||||
paddingLeft: 68, // 40(icon) + 12(gap) + 16(padding)
|
||||
paddingRight: 16,
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
backgroundColor: '#F0F0F0',
|
||||
},
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import { getTabBarBottomPadding } from '@/constants/TabBar';
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
||||
import { appStoreReviewService } from '@/services/appStoreReview';
|
||||
import { deleteWeightRecord, fetchWeightHistory, updateUserProfile, updateWeightRecord, WeightHistoryItem } from '@/store/userSlice';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -107,6 +108,13 @@ export default function WeightRecordsPage() {
|
||||
if (pickerType === 'current') {
|
||||
// Update current weight in profile and add weight record
|
||||
await dispatch(updateUserProfile({ weight: weight }) as any);
|
||||
|
||||
// 记录体重后尝试请求应用评分(延迟1秒,避免阻塞主流程)
|
||||
setTimeout(() => {
|
||||
appStoreReviewService.requestReview().catch((error) => {
|
||||
console.error('应用评分请求失败:', error);
|
||||
});
|
||||
}, 1000);
|
||||
} else if (pickerType === 'initial') {
|
||||
// Update initial weight in profile
|
||||
console.log('更新初始体重');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useWaterDataByDate } from '@/hooks/useWaterData';
|
||||
import { appStoreReviewService } from '@/services/appStoreReview';
|
||||
import { getQuickWaterAmount } from '@/utils/userPreferences';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -139,6 +140,9 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
|
||||
// 如果有选中日期,则为该日期添加记录;否则为今天添加记录
|
||||
const recordedAt = dayjs().toISOString()
|
||||
await addWaterRecord(waterAmount, recordedAt);
|
||||
|
||||
// 记录饮水后尝试请求应用评分
|
||||
await appStoreReviewService.requestReview();
|
||||
};
|
||||
|
||||
// 处理卡片点击 - 跳转到饮水详情页面
|
||||
|
||||
@@ -180,12 +180,12 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails
|
||||
const hours = Math.floor(timeDiffMinutes / 60);
|
||||
const minutes = timeDiffMinutes % 60;
|
||||
const formatted =
|
||||
hours > 0 ? `${hours}小时${minutes > 0 ? `${minutes}分钟` : ''}` : `${minutes}分钟`;
|
||||
hours > 0 ? `${hours}:${minutes > 0 ? `${minutes}` : ''}` : `${minutes}`;
|
||||
|
||||
return (
|
||||
<View style={[styles.statusChip, styles.statusChipUpcoming]}>
|
||||
<Ionicons name="time-outline" size={14} color="#fff" />
|
||||
<ThemedText style={styles.statusChipText}>{t('medications.card.status.remaining', { time: formatted })}</ThemedText>
|
||||
<Ionicons name="time-outline" size={10} color="#fff" />
|
||||
<ThemedText style={styles.statusChipText}>{formatted}</ThemedText>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -365,8 +365,9 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
maxWidth: '70%',
|
||||
},
|
||||
cardDosage: {
|
||||
fontSize: 12,
|
||||
@@ -466,6 +467,7 @@ const styles = StyleSheet.create({
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
backgroundColor: '#1363FF',
|
||||
zIndex: 1
|
||||
},
|
||||
statusChipUpcoming: {
|
||||
backgroundColor: '#1363FF',
|
||||
@@ -474,7 +476,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: '#FF3B30',
|
||||
},
|
||||
statusChipText: {
|
||||
fontSize: 10,
|
||||
fontSize: 9,
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
},
|
||||
|
||||
20
ios/AppStoreReviewManager.m
Normal file
20
ios/AppStoreReviewManager.m
Normal file
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// AppStoreReviewManager.m
|
||||
// OutLive
|
||||
//
|
||||
// Objective-C 桥接文件:将 Swift 类暴露给 React Native
|
||||
//
|
||||
|
||||
#import <React/RCTBridgeModule.h>
|
||||
|
||||
@interface RCT_EXTERN_MODULE(AppStoreReviewManager, NSObject)
|
||||
|
||||
// 请求应用内评分
|
||||
RCT_EXTERN_METHOD(requestReview:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
// 检查是否可以请求评分
|
||||
RCT_EXTERN_METHOD(canRequestReview:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
@end
|
||||
85
ios/AppStoreReviewManager.swift
Normal file
85
ios/AppStoreReviewManager.swift
Normal file
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// AppStoreReviewManager.swift
|
||||
// OutLive
|
||||
//
|
||||
// iOS 原生模块:应用内评分管理
|
||||
// 使用 StoreKit 的 SKStoreReviewController 来请求应用评分
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import StoreKit
|
||||
import React
|
||||
|
||||
@objc(AppStoreReviewManager)
|
||||
class AppStoreReviewManager: NSObject {
|
||||
|
||||
@objc
|
||||
static func moduleName() -> String! {
|
||||
return "AppStoreReviewManager"
|
||||
}
|
||||
|
||||
/// 请求应用内评分
|
||||
/// 这是唯一的公开方法,由 JavaScript 层调用
|
||||
/// 注意:iOS 系统会自动管理评分请求的频率限制
|
||||
@objc
|
||||
func requestReview(
|
||||
_ resolver: @escaping RCTPromiseResolveBlock,
|
||||
rejecter: @escaping RCTPromiseRejectBlock
|
||||
) {
|
||||
DispatchQueue.main.async {
|
||||
// 检查 iOS 版本(SKStoreReviewController 需要 iOS 14.0+)
|
||||
if #available(iOS 14.0, *) {
|
||||
// 获取当前活动的场景
|
||||
if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
|
||||
// 请求评分
|
||||
SKStoreReviewController.requestReview(in: scene)
|
||||
|
||||
resolver([
|
||||
"success": true,
|
||||
"message": "Review request sent successfully"
|
||||
])
|
||||
} else {
|
||||
rejecter(
|
||||
"NO_SCENE",
|
||||
"Unable to find active window scene",
|
||||
nil
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// iOS 14 以下不支持,返回错误
|
||||
rejecter(
|
||||
"VERSION_NOT_SUPPORTED",
|
||||
"SKStoreReviewController requires iOS 14.0 or later",
|
||||
nil
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查是否可以请求评分
|
||||
/// 虽然 iOS 系统会自动限制,但这个方法可以让 JS 层做额外判断
|
||||
@objc
|
||||
func canRequestReview(
|
||||
_ resolver: @escaping RCTPromiseResolveBlock,
|
||||
rejecter: @escaping RCTPromiseRejectBlock
|
||||
) {
|
||||
// iOS 14.0+ 支持应用内评分
|
||||
if #available(iOS 14.0, *) {
|
||||
resolver([
|
||||
"canRequest": true,
|
||||
"systemVersion": UIDevice.current.systemVersion
|
||||
])
|
||||
} else {
|
||||
resolver([
|
||||
"canRequest": false,
|
||||
"systemVersion": UIDevice.current.systemVersion,
|
||||
"reason": "Requires iOS 14.0 or later"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
static func requiresMainQueueSetup() -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 60;
|
||||
objectVersion = 70;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -13,6 +13,8 @@
|
||||
792C52592EA880A7002F3F09 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 792C52582EA880A7002F3F09 /* StoreKit.framework */; };
|
||||
792C52622EB05B8F002F3F09 /* NativeToastManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 792C52602EB05B8F002F3F09 /* NativeToastManager.m */; };
|
||||
792C52632EB05B8F002F3F09 /* NativeToastManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792C52612EB05B8F002F3F09 /* NativeToastManager.swift */; };
|
||||
794DD5D62ED3E3BB0046E2B4 /* AppStoreReviewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 794DD5D42ED3E3BB0046E2B4 /* AppStoreReviewManager.m */; };
|
||||
794DD5D72ED3E3BB0046E2B4 /* AppStoreReviewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794DD5D52ED3E3BB0046E2B4 /* AppStoreReviewManager.swift */; };
|
||||
79B2CB702E7B954600B51753 /* OutLive-Bridging-Header.h in Sources */ = {isa = PBXBuildFile; fileRef = F11748442D0722820044C1D9 /* OutLive-Bridging-Header.h */; };
|
||||
79B2CB732E7B954F00B51753 /* HealthKitManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 79B2CB712E7B954F00B51753 /* HealthKitManager.m */; };
|
||||
79B2CB742E7B954F00B51753 /* HealthKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79B2CB722E7B954F00B51753 /* HealthKitManager.swift */; };
|
||||
@@ -64,6 +66,8 @@
|
||||
792C52582EA880A7002F3F09 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
||||
792C52602EB05B8F002F3F09 /* NativeToastManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = NativeToastManager.m; path = OutLive/NativeToastManager.m; sourceTree = "<group>"; };
|
||||
792C52612EB05B8F002F3F09 /* NativeToastManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = NativeToastManager.swift; path = OutLive/NativeToastManager.swift; sourceTree = "<group>"; };
|
||||
794DD5D42ED3E3BB0046E2B4 /* AppStoreReviewManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppStoreReviewManager.m; sourceTree = "<group>"; };
|
||||
794DD5D52ED3E3BB0046E2B4 /* AppStoreReviewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreReviewManager.swift; sourceTree = "<group>"; };
|
||||
79B2CB712E7B954F00B51753 /* HealthKitManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = HealthKitManager.m; path = OutLive/HealthKitManager.m; sourceTree = "<group>"; };
|
||||
79B2CB722E7B954F00B51753 /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HealthKitManager.swift; path = OutLive/HealthKitManager.swift; sourceTree = "<group>"; };
|
||||
79E80BA22EC5D92A004425BE /* medicineExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = medicineExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -87,7 +91,7 @@
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
79E80BBB2EC5D92B004425BE /* Exceptions for "medicine" folder in "medicineExtension" target */ = {
|
||||
79E80BBB2EC5D92B004425BE /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
@@ -97,18 +101,7 @@
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
79E80BA72EC5D92A004425BE /* medicine */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
79E80BBB2EC5D92B004425BE /* Exceptions for "medicine" folder in "medicineExtension" target */,
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = medicine;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
79E80BA72EC5D92A004425BE /* medicine */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (79E80BBB2EC5D92B004425BE /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = medicine; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -177,6 +170,8 @@
|
||||
83CBB9F61A601CBA00E9B192 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
794DD5D42ED3E3BB0046E2B4 /* AppStoreReviewManager.m */,
|
||||
794DD5D52ED3E3BB0046E2B4 /* AppStoreReviewManager.swift */,
|
||||
79E80BFB2EC5E127004425BE /* AppGroupUserDefaultsManager.h */,
|
||||
79E80BFC2EC5E127004425BE /* AppGroupUserDefaultsManager.m */,
|
||||
79E80BFD2EC5E127004425BE /* WidgetManager.h */,
|
||||
@@ -501,6 +496,8 @@
|
||||
B6B9273B2FD4F4A800C6391C /* BackgroundTaskBridge.swift in Sources */,
|
||||
79B2CB702E7B954600B51753 /* OutLive-Bridging-Header.h in Sources */,
|
||||
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
|
||||
794DD5D62ED3E3BB0046E2B4 /* AppStoreReviewManager.m in Sources */,
|
||||
794DD5D72ED3E3BB0046E2B4 /* AppStoreReviewManager.swift in Sources */,
|
||||
32476CAEFFCE691C1634B0A4 /* ExpoModulesProvider.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
||||
20
ios/OutLive/AppStoreReviewManager.m
Normal file
20
ios/OutLive/AppStoreReviewManager.m
Normal file
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// AppStoreReviewManager.m
|
||||
// OutLive
|
||||
//
|
||||
// Objective-C 桥接文件:将 Swift 类暴露给 React Native
|
||||
//
|
||||
|
||||
#import <React/RCTBridgeModule.h>
|
||||
|
||||
@interface RCT_EXTERN_MODULE(AppStoreReviewManager, NSObject)
|
||||
|
||||
// 请求应用内评分
|
||||
RCT_EXTERN_METHOD(requestReview:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
// 检查是否可以请求评分
|
||||
RCT_EXTERN_METHOD(canRequestReview:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
@end
|
||||
85
ios/OutLive/AppStoreReviewManager.swift
Normal file
85
ios/OutLive/AppStoreReviewManager.swift
Normal file
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// AppStoreReviewManager.swift
|
||||
// OutLive
|
||||
//
|
||||
// iOS 原生模块:应用内评分管理
|
||||
// 使用 StoreKit 的 SKStoreReviewController 来请求应用评分
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import StoreKit
|
||||
import React
|
||||
|
||||
@objc(AppStoreReviewManager)
|
||||
class AppStoreReviewManager: NSObject {
|
||||
|
||||
@objc
|
||||
static func moduleName() -> String! {
|
||||
return "AppStoreReviewManager"
|
||||
}
|
||||
|
||||
/// 请求应用内评分
|
||||
/// 这是唯一的公开方法,由 JavaScript 层调用
|
||||
/// 注意:iOS 系统会自动管理评分请求的频率限制
|
||||
@objc
|
||||
func requestReview(
|
||||
_ resolver: @escaping RCTPromiseResolveBlock,
|
||||
rejecter: @escaping RCTPromiseRejectBlock
|
||||
) {
|
||||
DispatchQueue.main.async {
|
||||
// 检查 iOS 版本(SKStoreReviewController 需要 iOS 14.0+)
|
||||
if #available(iOS 14.0, *) {
|
||||
// 获取当前活动的场景
|
||||
if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
|
||||
// 请求评分
|
||||
SKStoreReviewController.requestReview(in: scene)
|
||||
|
||||
resolver([
|
||||
"success": true,
|
||||
"message": "Review request sent successfully"
|
||||
])
|
||||
} else {
|
||||
rejecter(
|
||||
"NO_SCENE",
|
||||
"Unable to find active window scene",
|
||||
nil
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// iOS 14 以下不支持,返回错误
|
||||
rejecter(
|
||||
"VERSION_NOT_SUPPORTED",
|
||||
"SKStoreReviewController requires iOS 14.0 or later",
|
||||
nil
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查是否可以请求评分
|
||||
/// 虽然 iOS 系统会自动限制,但这个方法可以让 JS 层做额外判断
|
||||
@objc
|
||||
func canRequestReview(
|
||||
_ resolver: @escaping RCTPromiseResolveBlock,
|
||||
rejecter: @escaping RCTPromiseRejectBlock
|
||||
) {
|
||||
// iOS 14.0+ 支持应用内评分
|
||||
if #available(iOS 14.0, *) {
|
||||
resolver([
|
||||
"canRequest": true,
|
||||
"systemVersion": UIDevice.current.systemVersion
|
||||
])
|
||||
} else {
|
||||
resolver([
|
||||
"canRequest": false,
|
||||
"systemVersion": UIDevice.current.systemVersion,
|
||||
"reason": "Requires iOS 14.0 or later"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
static func requiresMainQueueSetup() -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
206
ios/Podfile.lock
206
ios/Podfile.lock
@@ -2679,127 +2679,127 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
EXApplication: a9d1c46d473d36f61302a9a81db2379441f3f094
|
||||
EXConstants: e6e50cdfcb4524f40121d1fdcff24e97b7dcd2fd
|
||||
EXImageLoader: e501c001bc40b8326605e82e6e80363c80fe06b5
|
||||
EXNotifications: 7aab54f0e5f3023122bc95699eaff7c52bacb559
|
||||
Expo: 795e9f87aca407bf92895d54ae5f7777fc1f3fbc
|
||||
ExpoAppleAuthentication: 414e4316f8e25a2afbc3943cf725579c910f24b8
|
||||
ExpoAsset: 3c3b7dd9b1318846a02ef05ce420e63d542aeb9f
|
||||
ExpoBackgroundTask: e048da30cd2d669c5ba20d5d704bee8dd6da320c
|
||||
ExpoBlur: b5b7a26572b3c33a11f0b2aa2f95c17c4c393b76
|
||||
ExpoCamera: 6d0c5bc68bc8de669f1ecd4242284de0827c4431
|
||||
ExpoFileSystem: 56f081328f5b48a6dcc8302eee51d4f2d9d0049b
|
||||
ExpoFont: b881d43057dceb7b31ff767b24f612609e80f60f
|
||||
ExpoGlassEffect: d35ec1a8e9d84492f23755d3020a6a81a20bd585
|
||||
ExpoHaptics: b48d913e7e5f23816c6f130e525c9a6501b160b5
|
||||
ExpoHead: aa5f5a8afaa9bd4969bfdd6d5b76681e2490fe5b
|
||||
ExpoImage: 6eb842cd07817402640545c41884dd7f5fbfbca5
|
||||
ExpoImagePicker: bd0a5c81d7734548f6908a480609257e85d19ea8
|
||||
ExpoKeepAwake: 3f5e3ac53627849174f3603271df8e08f174ed4a
|
||||
ExpoLinearGradient: f9e7182e5253d53b2de4134b69d70bbfc2d50588
|
||||
ExpoLinking: f5c171877e118e792cb9a77e5ade0b080d899b14
|
||||
ExpoLocalization: 6c6f0f89ad2822001ab0bc2eb6d4d980c77f080c
|
||||
ExpoMediaLibrary: 648cee3f5dcba13410ec9cc8ac9a426e89a61a31
|
||||
ExpoModulesCore: ed46799cfdf75784ee3ca37dac982c5298683e83
|
||||
ExpoQuickActions: 62b9db8a20618be1cc19efa3b562ac963c803d58
|
||||
ExpoSplashScreen: 9d2ff8fa08f2c00336a83f93bebffed3a8312519
|
||||
ExpoSQLite: f9d1202877e12bfa78a58309a3977ee4ea0b1314
|
||||
ExpoSymbols: ef7b8ac77ac2d496b1bc3f0f7daf5e19c3a9933a
|
||||
ExpoSystemUI: 9441d46a8efbf9224d1b2e6b18042452ffd0ed79
|
||||
ExpoUI: 821b058da921ea4aa6172b36d080991ea6fb2fae
|
||||
ExpoWebBrowser: 51218ce6ef35ea769e33409aac87fea3df4b919d
|
||||
EXTaskManager: 53f87ed11659341c3f3f02c0041498ef293f5684
|
||||
EXApplication: 296622817d459f46b6c5fe8691f4aac44d2b79e7
|
||||
EXConstants: fd688cef4e401dcf798a021cfb5d87c890c30ba3
|
||||
EXImageLoader: 189e3476581efe3ad4d1d3fb4735b7179eb26f05
|
||||
EXNotifications: 7cff475adb5d7a255a9ea46bbd2589cb3b454506
|
||||
Expo: 27ae59be9be4feab2b1c1ae06550752c524ca558
|
||||
ExpoAppleAuthentication: bc9de6e9ff3340604213ab9031d4c4f7f802623e
|
||||
ExpoAsset: 9ba6fbd677fb8e241a3899ac00fa735bc911eadf
|
||||
ExpoBackgroundTask: e0d201d38539c571efc5f9cb661fae8ab36ed61b
|
||||
ExpoBlur: 2dd8f64aa31f5d405652c21d3deb2d2588b1852f
|
||||
ExpoCamera: e75f6807a2c047f3338bbadd101af4c71a1d13a5
|
||||
ExpoFileSystem: b79eadbda7b7f285f378f95f959cc9313a1c9c61
|
||||
ExpoFont: cf9d90ec1d3b97c4f513211905724c8171f82961
|
||||
ExpoGlassEffect: 779c46bd04ea47ba4726efb73267b5bcc6abd664
|
||||
ExpoHaptics: 807476b0c39e9d82b7270349d6487928ce32df84
|
||||
ExpoHead: e317214fa14edeaf17748d39ec9e550a3d1194fb
|
||||
ExpoImage: 9c3428921c536ab29e5c6721d001ad5c1f469566
|
||||
ExpoImagePicker: d251aab45a1b1857e4156fed88511b278b4eee1c
|
||||
ExpoKeepAwake: 1a2e820692e933c94a565ec3fbbe38ac31658ffe
|
||||
ExpoLinearGradient: a464898cb95153125e3b81894fd479bcb1c7dd27
|
||||
ExpoLinking: f051f28e50ea9269ff539317c166adec81d9342d
|
||||
ExpoLocalization: b852a5d8ec14c5349c1593eca87896b5b3ebfcca
|
||||
ExpoMediaLibrary: 641a6952299b395159ccd459bd8f5f6764bf55fe
|
||||
ExpoModulesCore: 5f20603cf25698682d7c43c05fbba8c748b189d2
|
||||
ExpoQuickActions: 31a70aa6a606128de4416a4830e09cfabfe6667f
|
||||
ExpoSplashScreen: cbb839de72110dea1851dd3e85080b7923af2540
|
||||
ExpoSQLite: 7fa091ba5562474093fef09be644161a65e11b3f
|
||||
ExpoSymbols: 1ae04ce686de719b9720453b988d8bc5bf776c68
|
||||
ExpoSystemUI: 2761aa6875849af83286364811d46e8ed8ea64c7
|
||||
ExpoUI: b99a1d1ef5352a60bebf4f4fd3a50d2f896ae804
|
||||
ExpoWebBrowser: d04a0d6247a0bea4519fbc2ea816610019ad83e0
|
||||
EXTaskManager: cbbb80cbccea6487ccca0631809fbba2ed3e5271
|
||||
FBLazyVector: e95a291ad2dadb88e42b06e0c5fb8262de53ec12
|
||||
hermes-engine: 9f4dfe93326146a1c99eb535b1cb0b857a3cd172
|
||||
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
|
||||
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
lottie-ios: a881093fab623c467d3bce374367755c272bdd59
|
||||
lottie-react-native: 97a11537edc72d0763edab0c83e8cc8a0b9d8484
|
||||
lottie-react-native: cbe3d931a7c24f7891a8e8032c2bb9b2373c4b9c
|
||||
PurchasesHybridCommon: b7b4eafb55fbaaac19b4c36d4082657a3f0d8490
|
||||
RCTDeprecation: 943572d4be82d480a48f4884f670135ae30bf990
|
||||
RCTRequired: 8f3cfc90cc25cf6e420ddb3e7caaaabc57df6043
|
||||
RCTTypeSafety: 16a4144ca3f959583ab019b57d5633df10b5e97c
|
||||
React: 914f8695f9bf38e6418228c2ffb70021e559f92f
|
||||
React-callinvoker: 1c0808402aee0c6d4a0d8e7220ce6547af9fba71
|
||||
React-Core: 4ae98f9e8135b8ddbd7c98730afb6fdae883db90
|
||||
React-Core-prebuilt: 8f4cca589c14e8cf8fc6db4587ef1c2056b5c151
|
||||
React-CoreModules: e878a90bb19b8f3851818af997dbae3b3b0a27ac
|
||||
React-cxxreact: 28af9844f6dc87be1385ab521fbfb3746f19563c
|
||||
React-Core: c61410ef0ca6055e204a963992e363227e0fd1c5
|
||||
React-Core-prebuilt: 02f0ad625ddd47463c009c2d0c5dd35c0d982599
|
||||
React-CoreModules: 1f6d1744b5f9f2ec684a4bb5ced25370f87e5382
|
||||
React-cxxreact: 3af79478e8187b63ffc22b794cd42d3fc1f1f2da
|
||||
React-debug: 6328c2228e268846161f10082e80dc69eac2e90a
|
||||
React-defaultsnativemodule: afc9d809ec75780f39464a6949c07987fbea488c
|
||||
React-domnativemodule: 91a233260411d41f27f67aa1358b7f9f0bfd101d
|
||||
React-Fabric: 21f349b5e93f305a3c38c885902683a9c79cf983
|
||||
React-FabricComponents: 47ac634cc9ecc64b30a9997192f510eebe4177e4
|
||||
React-FabricImage: 21873acd6d4a51a0b97c133141051c7acb11cc86
|
||||
React-featureflags: 653f469f0c3c9dc271d610373e3b6e66a9fd847d
|
||||
React-featureflagsnativemodule: c91a8a3880e0f4838286402241ead47db43aed28
|
||||
React-graphics: b4bdb0f635b8048c652a5d2b73eb8b1ddd950f24
|
||||
React-hermes: fcfad3b917400f49026f3232561e039c9d1c34bf
|
||||
React-idlecallbacksnativemodule: 8cb83207e39f8179ac1d344b6177c6ab3ccebcdc
|
||||
React-ImageManager: 396128004783fc510e629124dce682d38d1088e7
|
||||
React-jserrorhandler: b58b788d788cdbf8bda7db74a88ebfcffc8a0795
|
||||
React-jsi: d2c3f8555175371c02da6dfe7ed1b64b55a9d6c0
|
||||
React-jsiexecutor: ba537434eb45ee018b590ed7d29ee233fddb8669
|
||||
React-jsinspector: f21b6654baf96cb9f71748844a32468a5f73ad51
|
||||
React-jsinspectorcdp: 3f8be4830694c3c1c39442e50f8db877966d43f0
|
||||
React-jsinspectornetwork: 70e41469565712ad60e11d9c8b8f999b9f7f61eb
|
||||
React-jsinspectortracing: eccf9bfa4ec7f130d514f215cfb2222dc3c0e270
|
||||
React-jsitooling: b376a695f5a507627f7934748533b24eed1751ca
|
||||
React-jsitracing: 5c8c3273dda2d95191cc0612fb5e71c4d9018d2a
|
||||
React-logger: c3e2f8a2e284341205f61eef3d4677ab5a309dfd
|
||||
React-Mapbuffer: 603c18db65844bb81dbe62fee8fcc976eaeb7108
|
||||
React-microtasksnativemodule: d77e0c426fce34c23227394c96ca1033b30c813c
|
||||
react-native-render-html: 984dfe2294163d04bf5fe25d7c9f122e60e05ebe
|
||||
react-native-safe-area-context: add9b4ba236fe95ec600604d0fc72f395433dd59
|
||||
react-native-view-shot: 26174e54ec6b4b7c5d70b86964b747919759adc1
|
||||
react-native-voice: f5e8eec2278451d0017eb6a30a6ccc726aca34e0
|
||||
react-native-webview: a4f0775a31b73cf13cfc3d2d2b119aa94ec76e49
|
||||
React-NativeModulesApple: 1664340b8750d64e0ef3907c5e53d9481f74bcbd
|
||||
React-defaultsnativemodule: d635ef36d755321e5d6fc065bd166b2c5a0e9833
|
||||
React-domnativemodule: dd28f6d96cd21236e020be2eff6fe0b7d4ec3b66
|
||||
React-Fabric: 2e32c3fdbb1fbcf5fde54607e3abe453c6652ce2
|
||||
React-FabricComponents: 5ed0cdb81f6b91656cb4d3be432feaa28a58071a
|
||||
React-FabricImage: 2bc714f818cb24e454f5d3961864373271b2faf8
|
||||
React-featureflags: 847642f41fa71ad4eec5e0351badebcad4fe6171
|
||||
React-featureflagsnativemodule: c868a544b2c626fa337bcbd364b1befe749f0d3f
|
||||
React-graphics: 192ec701def5b3f2a07db2814dfba5a44986cff6
|
||||
React-hermes: e875778b496c86d07ab2ccaa36a9505d248a254b
|
||||
React-idlecallbacksnativemodule: 4d57965cdf82c14ee3b337189836cd8491632b76
|
||||
React-ImageManager: bd0b99e370b13de82c9cd15f0f08144ff3de079e
|
||||
React-jserrorhandler: a2fdef4cbcfdcdf3fa9f5d1f7190f7fd4535248d
|
||||
React-jsi: 89d43d1e7d4d0663f8ba67e0b39eb4e4672c27de
|
||||
React-jsiexecutor: abe4874aaab90dfee5dec480680220b2f8af07e3
|
||||
React-jsinspector: a0b3e051aef842b0b2be2353790ae2b2a5a65a8f
|
||||
React-jsinspectorcdp: 6346013b2247c6263fbf5199adf4a8751e53bd89
|
||||
React-jsinspectornetwork: 26281aa50d49fc1ec93abf981d934698fa95714f
|
||||
React-jsinspectortracing: 55eedf6d57540507570259a778663b90060bbd6e
|
||||
React-jsitooling: 0e001113fa56d8498aa8ac28437ac0d36348e51a
|
||||
React-jsitracing: b713793eb8a5bbc4d86a84e9d9e5023c0f58cbaf
|
||||
React-logger: 50fdb9a8236da90c0b1072da5c32ee03aeb5bf28
|
||||
React-Mapbuffer: 9050ee10c19f4f7fca8963d0211b2854d624973e
|
||||
React-microtasksnativemodule: f775db9e991c6f3b8ccbc02bfcde22770f96e23b
|
||||
react-native-render-html: 5afc4751f1a98621b3009432ef84c47019dcb2bd
|
||||
react-native-safe-area-context: 42a1b4f8774b577d03b53de7326e3d5757fe9513
|
||||
react-native-view-shot: fb3c0774edb448f42705491802a455beac1502a2
|
||||
react-native-voice: 908a0eba96c8c3d643e4f98b7232c6557d0a6f9c
|
||||
react-native-webview: b29007f4723bca10872028067b07abacfa1cb35a
|
||||
React-NativeModulesApple: 8969913947d5b576de4ed371a939455a8daf28aa
|
||||
React-oscompat: ce47230ed20185e91de62d8c6d139ae61763d09c
|
||||
React-perflogger: b1af3cfb3f095f819b2814910000392a8e17ba9f
|
||||
React-performancetimeline: f9ec65b77bcadbc7bd8b47a6f4b4b697da7b1490
|
||||
React-perflogger: 02b010e665772c7dcb859d85d44c1bfc5ac7c0e4
|
||||
React-performancetimeline: 130db956b5a83aa4fb41ddf5ae68da89f3fb1526
|
||||
React-RCTActionSheet: 0b14875b3963e9124a5a29a45bd1b22df8803916
|
||||
React-RCTAnimation: 60f6eca214a62b9673f64db6df3830cee902b5af
|
||||
React-RCTAppDelegate: 37734b39bac108af30a0fd9d3e1149ec68b82c28
|
||||
React-RCTBlob: 83fbcbd57755caf021787324aac2fe9b028cc264
|
||||
React-RCTFabric: a05cb1df484008db3753c8b4a71e4c6d9f1e43a6
|
||||
React-RCTFBReactNativeSpec: d58d7ae9447020bbbac651e3b0674422aba18266
|
||||
React-RCTImage: 47aba3be7c6c64f956b7918ab933769602406aac
|
||||
React-RCTLinking: 2dbaa4df2e4523f68baa07936bd8efdfa34d5f31
|
||||
React-RCTNetwork: 1fca7455f9dedf7de2b95bec438da06680f3b000
|
||||
React-RCTRuntime: 17819dd1dfc8613efaf4cbb9d8686baae4a83e5b
|
||||
React-RCTSettings: 01bf91c856862354d3d2f642ccb82f3697a4284a
|
||||
React-RCTText: cb576a3797dcb64933613c522296a07eaafc0461
|
||||
React-RCTVibration: 560af8c086741f3525b8456a482cdbe27f9d098e
|
||||
React-RCTAnimation: a7b90fd2af7bb9c084428867445a1481a8cb112e
|
||||
React-RCTAppDelegate: 3262bedd01263f140ec62b7989f4355f57cec016
|
||||
React-RCTBlob: c17531368702f1ebed5d0ada75a7cf5915072a53
|
||||
React-RCTFabric: 6409edd8cfdc3133b6cc75636d3b858fdb1d11ea
|
||||
React-RCTFBReactNativeSpec: c004b27b4fa3bd85878ad2cf53de3bbec85da797
|
||||
React-RCTImage: c68078a120d0123f4f07a5ac77bea3bb10242f32
|
||||
React-RCTLinking: cf8f9391fe7fe471f96da3a5f0435235eca18c5b
|
||||
React-RCTNetwork: ca31f7c879355760c2d9832a06ee35f517938a20
|
||||
React-RCTRuntime: a6cf4a1e42754fc87f493e538f2ac6b820e45418
|
||||
React-RCTSettings: e0e140b2ff4bf86d34e9637f6316848fc00be035
|
||||
React-RCTText: 75915bace6f7877c03a840cc7b6c622fb62bfa6b
|
||||
React-RCTVibration: 25f26b85e5e432bb3c256f8b384f9269e9529f25
|
||||
React-rendererconsistency: 2dac03f448ff337235fd5820b10f81633328870d
|
||||
React-renderercss: c5c6b7a15948dd28facca39a18ac269073718490
|
||||
React-rendererdebug: 3c9d5e1634273f5a24d84cc5669f290ce0bdc812
|
||||
React-RuntimeApple: 887637d1e12ea8262df7d32bc100467df2302613
|
||||
React-RuntimeCore: 91f779835dc4f8f84777fe5dd24f1a22f96454e4
|
||||
React-runtimeexecutor: 8bb6b738f37b0ada4a6269e6f8ab1133dea0285c
|
||||
React-RuntimeHermes: 4cb93de9fa8b1cc753d200dbe61a01b9ec5f5562
|
||||
React-runtimescheduler: 83dc28f530bfbd2fce84ed13aa7feebdc24e5af7
|
||||
React-timing: 03c7217455d2bff459b27a3811be25796b600f47
|
||||
React-utils: 6d46795ae0444ec8a5d9a5f201157b286bf5250a
|
||||
ReactAppDependencyProvider: c277c5b231881ad4f00cd59e3aa0671b99d7ebee
|
||||
ReactCodegen: 4c44b74b77fc41ae25b9e2c7e9bd6e2bc772c23f
|
||||
ReactCommon: e6e232202a447d353e5531f2be82f50f47cbaa9a
|
||||
React-renderercss: 477da167bb96b5ac86d30c5d295412fb853f5453
|
||||
React-rendererdebug: 2a1798c6f3ef5f22d466df24c33653edbabb5b89
|
||||
React-RuntimeApple: 28cf4d8eb18432f6a21abbed7d801ab7f6b6f0b4
|
||||
React-RuntimeCore: 41bf0fd56a00de5660f222415af49879fa49c4f0
|
||||
React-runtimeexecutor: 1afb774dde3011348e8334be69d2f57a359ea43e
|
||||
React-RuntimeHermes: f3b158ea40e8212b1a723a68b4315e7a495c5fc6
|
||||
React-runtimescheduler: 3e1e2bec7300bae512533107d8e54c6e5c63fe0f
|
||||
React-timing: 6fa9883de2e41791e5dc4ec404e5e37f3f50e801
|
||||
React-utils: 6e2035b53d087927768649a11a26c4e092448e34
|
||||
ReactAppDependencyProvider: 1bcd3527ac0390a1c898c114f81ff954be35ed79
|
||||
ReactCodegen: 7d4593f7591f002d137fe40cef3f6c11f13c88cc
|
||||
ReactCommon: 08810150b1206cc44aecf5f6ae19af32f29151a8
|
||||
ReactNativeDependencies: 71ce9c28beb282aa720ea7b46980fff9669f428a
|
||||
RevenueCat: a51003d4cb33820cc504cf177c627832b462a98e
|
||||
RNCAsyncStorage: e85a99325df9eb0191a6ee2b2a842644c7eb29f4
|
||||
RNCMaskedView: 3c9d7586e2b9bbab573591dcb823918bc4668005
|
||||
RNCPicker: f97c908b7774248c1093ec3831ca70d338627bf7
|
||||
RNDateTimePicker: 6fdd63f5d1e0f21faf4cc8674957c52958a7efae
|
||||
RNDeviceInfo: 8b6fa8379062949dd79a009cf3d6b02a9c03ca59
|
||||
RNGestureHandler: 6a488ce85c88e82d8610db1108daf04e9b2d5162
|
||||
RNPurchases: e7d57c35ec94625f455981307c1487adde5e3188
|
||||
RNReanimated: 1c03486192caeabe2795787e4bb046116383be7a
|
||||
RNScreens: dd61bc3a3e6f6901ad833efa411917d44827cf51
|
||||
RNSentry: bf366a415176cb6971a5adac37bbe66dfea272f3
|
||||
RNSVG: 2825ee146e0f6a16221e852299943e4cceef4528
|
||||
RNWorklets: 83609071441ac7d623f1e0e63b9043f4f345e2a2
|
||||
RNCAsyncStorage: 3a4f5e2777dae1688b781a487923a08569e27fe4
|
||||
RNCMaskedView: d2578d41c59b936db122b2798ba37e4722d21035
|
||||
RNCPicker: a7170edbcbf8288de8edb2502e08e7fc757fa755
|
||||
RNDateTimePicker: be0e44bcb9ed0607c7c5f47dbedd88cf091f6791
|
||||
RNDeviceInfo: bcce8752b5043a623fe3c26789679b473f705d3c
|
||||
RNGestureHandler: 2914750df066d89bf9d8f48a10ad5f0051108ac3
|
||||
RNPurchases: 2569675abdc1dbc739f2eec0fa564a112cf860de
|
||||
RNReanimated: 3895a29fdf77bbe2a627e1ed599a5e5d1df76c29
|
||||
RNScreens: d8d6f1792f6e7ac12b0190d33d8d390efc0c1845
|
||||
RNSentry: 41979b419908128847ef662cc130a400b7576fa9
|
||||
RNSVG: 31d6639663c249b7d5abc9728dde2041eb2a3c34
|
||||
RNWorklets: 54d8dffb7f645873a58484658ddfd4bd1a9a0bc1
|
||||
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||
SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57
|
||||
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
|
||||
|
||||
118
scripts/check-app-review-setup.sh
Normal file
118
scripts/check-app-review-setup.sh
Normal file
@@ -0,0 +1,118 @@
|
||||
#!/bin/bash
|
||||
|
||||
# iOS 应用内评分功能配置检查脚本
|
||||
# 使用方法: bash scripts/check-app-review-setup.sh
|
||||
|
||||
echo "================================================"
|
||||
echo "iOS 应用内评分功能 - 配置检查"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 检查计数
|
||||
checks_passed=0
|
||||
checks_failed=0
|
||||
|
||||
# 函数:检查文件存在
|
||||
check_file() {
|
||||
local file=$1
|
||||
local description=$2
|
||||
|
||||
if [ -f "$file" ]; then
|
||||
echo -e "${GREEN}✓${NC} $description"
|
||||
echo " 路径: $file"
|
||||
((checks_passed++))
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}✗${NC} $description"
|
||||
echo " 路径: $file (文件不存在)"
|
||||
((checks_failed++))
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "步骤 1: 检查原生模块文件"
|
||||
echo "-----------------------------------"
|
||||
check_file "ios/OutLive/AppStoreReviewManager.swift" "Swift 实现文件"
|
||||
check_file "ios/OutLive/AppStoreReviewManager.m" "Objective-C 桥接文件"
|
||||
check_file "ios/OutLive/OutLive-Bridging-Header.h" "Bridging Header 文件"
|
||||
echo ""
|
||||
|
||||
echo "步骤 2: 检查服务层文件"
|
||||
echo "-----------------------------------"
|
||||
check_file "services/appStoreReview.ts" "评分请求管理服务"
|
||||
echo ""
|
||||
|
||||
echo "步骤 3: 检查 Redux 集成"
|
||||
echo "-----------------------------------"
|
||||
if check_file "store/challengesSlice.ts" "挑战 Slice 集成"; then
|
||||
if grep -q "appStoreReviewService" "store/challengesSlice.ts"; then
|
||||
echo -e " ${GREEN}✓${NC} 已集成应用评分服务"
|
||||
else
|
||||
echo -e " ${YELLOW}!${NC} 未检测到应用评分服务集成"
|
||||
((checks_failed++))
|
||||
fi
|
||||
fi
|
||||
|
||||
if check_file "store/medicationsSlice.ts" "用药 Slice 集成"; then
|
||||
if grep -q "appStoreReviewService" "store/medicationsSlice.ts"; then
|
||||
echo -e " ${GREEN}✓${NC} 已集成应用评分服务"
|
||||
else
|
||||
echo -e " ${YELLOW}!${NC} 未检测到应用评分服务集成"
|
||||
((checks_failed++))
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "步骤 4: 检查文档"
|
||||
echo "-----------------------------------"
|
||||
check_file "docs/app-store-review-implementation.md" "实现文档"
|
||||
check_file "docs/app-store-review-xcode-setup.md" "Xcode 配置指南"
|
||||
echo ""
|
||||
|
||||
# 总结
|
||||
echo "================================================"
|
||||
echo "检查总结"
|
||||
echo "================================================"
|
||||
echo -e "通过: ${GREEN}$checks_passed${NC}"
|
||||
echo -e "失败: ${RED}$checks_failed${NC}"
|
||||
echo ""
|
||||
|
||||
# 根据结果给出建议
|
||||
if [ $checks_failed -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ 所有文件检查通过!${NC}"
|
||||
echo ""
|
||||
echo "下一步操作:"
|
||||
echo "1. 打开 Xcode 项目:"
|
||||
echo " cd ios && open OutLive.xcworkspace"
|
||||
echo ""
|
||||
echo "2. 在 Xcode 中添加原生模块文件(详见文档):"
|
||||
echo " - AppStoreReviewManager.swift"
|
||||
echo " - AppStoreReviewManager.m"
|
||||
echo ""
|
||||
echo "3. 清理并重新构建:"
|
||||
echo " Product > Clean Build Folder (Shift+Cmd+K)"
|
||||
echo " Product > Build (Cmd+B)"
|
||||
echo ""
|
||||
echo "4. 运行应用进行测试"
|
||||
echo ""
|
||||
echo "详细步骤请参考: docs/app-store-review-xcode-setup.md"
|
||||
else
|
||||
echo -e "${RED}✗ 检查未通过,请修复以上问题${NC}"
|
||||
echo ""
|
||||
echo "常见问题:"
|
||||
echo "- 如果文件不存在,请确认文件是否被正确创建"
|
||||
echo "- 如果集成检查失败,请检查代码是否正确导入和使用服务"
|
||||
echo ""
|
||||
echo "获取帮助:"
|
||||
echo "- 查看实现文档: docs/app-store-review-implementation.md"
|
||||
echo "- 查看配置指南: docs/app-store-review-xcode-setup.md"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "================================================"
|
||||
165
services/appStoreReview.ts
Normal file
165
services/appStoreReview.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* App Store 应用内评分服务
|
||||
*
|
||||
* 功能:
|
||||
* 1. 调用 iOS 原生模块请求应用内评分
|
||||
* 2. 管理评分请求时间间隔(至少 14 天)
|
||||
* 3. 提供便捷的业务场景触发方法
|
||||
*
|
||||
* iOS 限制说明:
|
||||
* - iOS 系统会自动限制评分请求的频率(每年最多 3 次)
|
||||
* - 本服务额外实现 14 天间隔限制,确保不会过于频繁打扰用户
|
||||
*/
|
||||
|
||||
import * as kvStore from '@/utils/kvStore';
|
||||
import { NativeModules, Platform } from 'react-native';
|
||||
|
||||
// 原生模块
|
||||
const { AppStoreReviewManager } = NativeModules;
|
||||
|
||||
// 存储键
|
||||
const LAST_REVIEW_REQUEST_DATE_KEY = 'app_store_review_last_request_date';
|
||||
const MIN_DAYS_BETWEEN_REQUESTS = 14; // 最少间隔天数
|
||||
|
||||
/**
|
||||
* 检查是否可以请求评分
|
||||
* @returns Promise<boolean> 是否可以请求
|
||||
*/
|
||||
async function canRequestReview(): Promise<boolean> {
|
||||
// 只在 iOS 平台支持
|
||||
if (Platform.OS !== 'ios') {
|
||||
console.log('⚠️ App Store 评分仅支持 iOS 平台');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查原生模块是否可用
|
||||
if (!AppStoreReviewManager) {
|
||||
console.error('❌ AppStoreReviewManager 原生模块不可用');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查系统是否支持(iOS 14.0+)
|
||||
const systemCheck = await AppStoreReviewManager.canRequestReview();
|
||||
if (!systemCheck.canRequest) {
|
||||
console.log('⚠️ 系统不支持应用内评分:', systemCheck.reason);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查上次请求时间
|
||||
const lastRequestDate = await kvStore.getItem(LAST_REVIEW_REQUEST_DATE_KEY);
|
||||
if (lastRequestDate) {
|
||||
const daysSinceLastRequest = Math.floor(
|
||||
(Date.now() - parseInt(lastRequestDate, 10)) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
|
||||
if (daysSinceLastRequest < MIN_DAYS_BETWEEN_REQUESTS) {
|
||||
console.log(
|
||||
`⚠️ 距离上次评分请求仅 ${daysSinceLastRequest} 天,需要至少 ${MIN_DAYS_BETWEEN_REQUESTS} 天`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ 检查评分请求条件失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求应用内评分
|
||||
* @returns Promise<boolean> 是否成功发起请求
|
||||
*/
|
||||
async function requestReview(): Promise<boolean> {
|
||||
try {
|
||||
// 检查是否可以请求
|
||||
const canRequest = await canRequestReview();
|
||||
if (!canRequest) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 调用原生模块请求评分
|
||||
const result = await AppStoreReviewManager.requestReview();
|
||||
|
||||
if (result.success) {
|
||||
// 记录本次请求时间
|
||||
await kvStore.setItem(LAST_REVIEW_REQUEST_DATE_KEY, Date.now().toString());
|
||||
console.log('✅ 应用内评分请求已发送');
|
||||
return true;
|
||||
} else {
|
||||
console.error('❌ 应用内评分请求失败:', result);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 请求应用内评分时出错:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上次请求评分的时间
|
||||
* @returns Promise<Date | null> 上次请求时间,如果从未请求过则返回 null
|
||||
*/
|
||||
async function getLastRequestDate(): Promise<Date | null> {
|
||||
try {
|
||||
const lastRequestDate = await kvStore.getItem(LAST_REVIEW_REQUEST_DATE_KEY);
|
||||
if (lastRequestDate) {
|
||||
return new Date(parseInt(lastRequestDate, 10));
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('❌ 获取上次评分请求时间失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取距离下次可以请求评分的剩余天数
|
||||
* @returns Promise<number> 剩余天数,0 表示可以立即请求
|
||||
*/
|
||||
async function getDaysUntilNextRequest(): Promise<number> {
|
||||
try {
|
||||
const lastRequestDate = await getLastRequestDate();
|
||||
if (!lastRequestDate) {
|
||||
return 0; // 从未请求过,可以立即请求
|
||||
}
|
||||
|
||||
const daysSinceLastRequest = Math.floor(
|
||||
(Date.now() - lastRequestDate.getTime()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
|
||||
const daysRemaining = MIN_DAYS_BETWEEN_REQUESTS - daysSinceLastRequest;
|
||||
return Math.max(0, daysRemaining);
|
||||
} catch (error) {
|
||||
console.error('❌ 计算剩余天数失败:', error);
|
||||
return MIN_DAYS_BETWEEN_REQUESTS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置评分请求记录(仅用于测试)
|
||||
* ⚠️ 警告:不应在生产环境中调用此方法
|
||||
*/
|
||||
async function resetRequestHistory(): Promise<void> {
|
||||
try {
|
||||
await kvStore.removeItem(LAST_REVIEW_REQUEST_DATE_KEY);
|
||||
console.log('✅ 评分请求历史已重置');
|
||||
} catch (error) {
|
||||
console.error('❌ 重置评分请求历史失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出服务
|
||||
export const appStoreReviewService = {
|
||||
// 核心方法
|
||||
canRequestReview,
|
||||
requestReview,
|
||||
getLastRequestDate,
|
||||
getDaysUntilNextRequest,
|
||||
resetRequestHistory,
|
||||
};
|
||||
|
||||
// 常量导出
|
||||
export { MIN_DAYS_BETWEEN_REQUESTS };
|
||||
@@ -1,3 +1,4 @@
|
||||
import { appStoreReviewService } from '@/services/appStoreReview';
|
||||
import {
|
||||
type ChallengeDetailDto,
|
||||
type ChallengeListItemDto,
|
||||
@@ -113,6 +114,15 @@ export const joinChallenge = createAsyncThunk<{ id: string; progress: ChallengeP
|
||||
async (id, { rejectWithValue }) => {
|
||||
try {
|
||||
const progress = await joinChallengeApi(id);
|
||||
|
||||
// 用户成功加入挑战后,尝试请求应用评分
|
||||
// 使用 setTimeout 延迟执行,避免阻塞主流程
|
||||
setTimeout(() => {
|
||||
appStoreReviewService.requestReview().catch((error) => {
|
||||
console.error('应用评分请求失败:', error);
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return { id, progress };
|
||||
} catch (error) {
|
||||
return rejectWithValue(toErrorMessage(error));
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* 药物管理 Redux Slice
|
||||
*/
|
||||
|
||||
import { appStoreReviewService } from '@/services/appStoreReview';
|
||||
import * as medicationsApi from '@/services/medications';
|
||||
import type {
|
||||
DailyMedicationStats,
|
||||
@@ -605,6 +606,13 @@ const medicationsSlice = createSlice({
|
||||
console.error('Failed to sync medication data to widget:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 服药成功后请求应用评分(延迟1秒,避免阻塞主流程)
|
||||
setTimeout(() => {
|
||||
appStoreReviewService.requestReview().catch((error) => {
|
||||
console.error('应用评分请求失败:', error);
|
||||
});
|
||||
}, 1000);
|
||||
})
|
||||
.addCase(takeMedicationAction.rejected, (state, action) => {
|
||||
state.loading.takeMedication = false;
|
||||
|
||||
@@ -39,7 +39,7 @@ const DEFAULT_PREFERENCES: UserPreferences = {
|
||||
notificationEnabled: true, // 默认开启消息推送
|
||||
fitnessExerciseMinutesInfoDismissed: false, // 默认显示锻炼分钟说明
|
||||
fitnessActiveHoursInfoDismissed: false, // 默认显示活动小时说明
|
||||
waterReminderEnabled: true, // 默认关闭喝水提醒
|
||||
waterReminderEnabled: false, // 默认关闭喝水提醒
|
||||
waterReminderStartTime: '08:00', // 默认开始时间早上8点
|
||||
waterReminderEndTime: '22:00', // 默认结束时间晚上10点
|
||||
waterReminderInterval: 60, // 默认提醒间隔60分钟
|
||||
|
||||
Reference in New Issue
Block a user