Files
digital-pilates/services/appStoreReview.ts
richarjiang c1c9f22111 feat(review): 集成iOS应用内评分功能
- 新增iOS原生模块AppStoreReviewManager,封装StoreKit评分请求
- 实现appStoreReviewService服务层,管理评分请求时间间隔(14天)
- 在关键用户操作后触发评分请求:完成挑战、记录服药、记录体重、记录饮水
- 优化通知设置页面UI,改进设置项布局和视觉层次
- 调整用药卡片样式,优化状态显示和文字大小
- 新增配置检查脚本check-app-review-setup.sh
- 修改喝水提醒默认状态为关闭

评分请求策略:
- 仅iOS 14.0+支持
- 自动控制请求频率,避免过度打扰用户
- 延迟1秒执行,不阻塞主业务流程
- 所有评分请求均做错误处理,确保不影响核心功能
2025-11-24 10:06:18 +08:00

166 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 };