feat(membership): 实现会员系统和购买流程

- 创建 MembershipModalContext 统一管理会员弹窗
- 优化 MembershipModal 产品套餐展示和购买流程
- 集成 RevenueCat SDK 并初始化内购功能
- 在个人中心添加会员 Banner,引导非会员用户订阅
- 修复日志工具的循环引用问题,确保错误信息正确记录
- 版本更新至 1.0.20

新增了完整的会员购买流程,包括套餐选择、购买确认、购买恢复等功能。会员 Banner 仅对非会员用户展示,已是会员的用户不会看到。同时优化了错误日志记录,避免循环引用导致的序列化失败。
This commit is contained in:
richarjiang
2025-10-24 09:16:04 +08:00
parent b75a8991ac
commit 2e11f694f8
9 changed files with 437 additions and 186 deletions

View File

@@ -3,6 +3,7 @@ import CustomCheckBox from '@/components/ui/CheckBox';
import { USER_AGREEMENT_URL } from '@/constants/Agree';
// import { useAuth } from '@/contexts/AuthContext';
// import { UserApi } from '@/services';
import { log, logger } from '@/utils/logger';
import {
captureMessage,
captureMessageWithContext,
@@ -17,10 +18,8 @@ import {
ActivityIndicator,
Alert,
Dimensions,
Image,
Linking,
Modal,
Platform,
ScrollView,
StyleSheet,
Text,
@@ -39,70 +38,34 @@ interface MembershipModalProps {
interface MembershipPlan {
id: string;
title: string;
fallbackTitle: string;
subtitle: string;
price: string;
originalPrice?: string;
discount?: string;
type: 'weekly' | 'quarterly' | 'lifetime';
recommended?: boolean;
}
const DEFAULT_PLANS: MembershipPlan[] = [
{
id: 'com.ilookai.mind_gpt.Lifetime',
title: '终身会员',
subtitle: '每天0.01元\n永久有效',
price: '¥128',
id: 'com.anonymous.digitalpilates.membership.lifetime',
fallbackTitle: '终身会员',
subtitle: '一次投入,终身健康陪伴',
type: 'lifetime',
recommended: true,
},
{
id: 'com.ilookai.mind_gpt.ThreeMonths',
title: '季度会员',
subtitle: '每天1元\n连续包季',
price: '¥98',
id: 'com.anonymous.digitalpilates.membership.quarter',
fallbackTitle: '季度会员',
subtitle: '3个月蜕变计划见证身材变化',
type: 'quarterly',
},
{
id: 'weekly_membership',
title: '周会员',
subtitle: '新人专享\n连续包周',
price: '¥18',
id: 'com.anonymous.digitalpilates.membership.weekly',
fallbackTitle: '周会员',
subtitle: '7天体验开启健康第一步',
type: 'weekly',
},
];
// {
// identifier: 'com.ilookai.mind_gpt.Lifetime',
// description: '终身会员',
// title: '终身会员',
// price: 128,
// priceString: '¥128',
// pricePerWeek: 128,
// pricePerMonth: 128,
// pricePerYear: 128,
// }, {
// identifier: 'com.ilookai.mind_gpt.ThreeMonths',
// description: '季度会员',
// title: '季度会员',
// price: 98,
// priceString: '¥98',
// pricePerWeek: 98,
// pricePerMonth: 98,
// pricePerYear: 98,
// }, {
// identifier: 'weekly_membership',
// description: '周会员',
// title: '周会员',
// price: 18,
// priceString: '¥18',
// pricePerWeek: 18,
// pricePerMonth: 18,
// pricePerYear: 18,
// }
export function MembershipModal({ visible, onClose, onPurchaseSuccess }: MembershipModalProps) {
const [selectedProduct, setSelectedProduct] = useState<PurchasesStoreProduct | null>(null);
const [loading, setLoading] = useState(false);
@@ -121,13 +84,18 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
const getTipsContent = (product: PurchasesStoreProduct | null): string => {
if (!product) return '';
// 这里您可以根据不同的产品返回不同的提示内容
switch (product.identifier) {
case 'com.ilookai.mind_gpt.Lifetime':
return '一次购买,永久享受所有功能';
case 'com.ilookai.mind_gpt.ThreeMonths':
case 'weekly_membership':
return '到期后自动续费,可以随时随地取消';
const plan = DEFAULT_PLANS.find(item => item.id === product.identifier);
if (!plan) {
return '';
}
switch (plan.type) {
case 'lifetime':
return '终身陪伴,见证您的每一次健康蜕变';
case 'quarterly':
return '3个月科学计划让健康成为生活习惯';
case 'weekly':
return '7天体验期感受专业健康指导的力量';
default:
return '';
}
@@ -144,88 +112,99 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
useEffect(() => {
if (visible) {
setupPurchaseListener();
// 重置协议状态
setAgreementAccepted(false);
initPurchases();
} else {
// 弹窗关闭时移除监听器
removePurchaseListener();
setProducts([]);
setSelectedProduct(null);
setAgreementAccepted(false);
setLoading(false);
setRestoring(false);
}
// 组件卸载时确保移除监听器
return () => {
removePurchaseListener();
console.log('MembershipModal 购买监听器已清理');
log.info('MembershipModal 购买监听器已清理');
};
}, [visible]); // 依赖 visible 状态
const initPurchases = async () => {
if (Platform.OS === 'ios') {
Purchases.configure({
apiKey: 'appl_UXFtPsBsFIsBOxoNGXoPwpXhGYk'
});
} else if (Platform.OS === 'android') {
// Purchases.configure({
// apiKey: 'goog_ZqYxWbQvRgLzBnVfYdXcWbVn'
// });
}
capturePurchaseEvent('init', 'Purchases 初始化成功');
capturePurchaseEvent('init', '开始获取会员产品套餐');
try {
// if (user?.id) {
// await Purchases.logIn(user.id);
// captureMessageWithContext('用户登录 Purchases 成功', {
// userId: user.id
// });
// }
// 添加延迟,确保 RevenueCat SDK 完全初始化
await new Promise(resolve => setTimeout(resolve, 500));
const offerings = await Purchases.getOfferings();
console.log('offerings', offerings);
log.info('获取产品套餐', { offerings });
captureMessageWithContext('获取产品套餐成功', {
logger.info('获取产品套餐成功', {
currentOffering: offerings.current?.identifier || null,
availablePackagesCount: offerings.current?.availablePackages.length || 0,
allOfferingsCount: Object.keys(offerings.all).length
allOfferingsCount: Object.keys(offerings.all).length,
});
if (offerings.current !== null && offerings.current.availablePackages.length !== 0) {
// 当前活跃的套餐存在
const packages = offerings.current.availablePackages;
console.log('Available packages:', packages);
// packages 是一个数组,包含 PurchasePackage 对象,每个对象都有 product 信息
// 可以根据这些信息来渲染你的 UI例如价格、标题等
const sortedProducts = packages.sort((a, b) => b.product.price - a.product.price).map(pkg => pkg.product);
setProducts(sortedProducts);
const packages = offerings.current?.availablePackages ?? [];
// 获取产品后,检查用户的购买记录并自动选中对应套餐
await checkAndSelectActivePlan(sortedProducts);
} else {
console.warn('No active offerings found or no packages available.');
captureMessageWithContext('没有找到可用的产品套餐', {
if (packages.length === 0) {
log.warn('没有找到可用的产品套餐', {
hasCurrentOffering: offerings.current !== null,
packagesLength: offerings.current?.availablePackages.length || 0
packagesLength: offerings.current?.availablePackages.length || 0,
});
logger.info('没有找到可用的产品套餐', {
hasCurrentOffering: offerings.current !== null,
packagesLength: offerings.current?.availablePackages.length || 0,
});
setProducts([]);
setSelectedProduct(null);
return;
}
const matchedProducts = packages
.map(pkg => pkg.product)
.filter(product => DEFAULT_PLANS.some(plan => plan.id === product.identifier));
const orderedProducts = DEFAULT_PLANS
.map(plan => matchedProducts.find(product => product.identifier === plan.id))
.filter((product): product is PurchasesStoreProduct => Boolean(product));
const fallbackProducts = packages.map(pkg => pkg.product);
const productsToUse = orderedProducts.length > 0 ? orderedProducts : fallbackProducts;
log.info('productsToUse', productsToUse)
setProducts(productsToUse);
// 获取产品后,检查用户的购买记录并自动选中对应套餐
await checkAndSelectActivePlan(productsToUse);
setSelectedProduct(current => current ?? (productsToUse[0] ?? null));
} catch (e: any) {
console.log('error', e);
// Error fetching customer info
// 安全地处理错误对象,避免循环引用
const errorData = {
message: e?.message || '未知错误',
code: e?.code || null,
name: e?.name || 'Error',
// 只包含基本的错误信息,避免可能的循环引用
};
log.error('获取产品套餐失败', { error: errorData });
captureException(e);
// captureMessageWithContext('初始化 Purchases 失败', {
// error: e.message || '未知错误',
// userId: user?.id || null
// });
capturePurchaseEvent('error', '获取产品套餐失败', errorData);
// 设置空状态,避免界面卡在加载状态
setProducts([]);
setSelectedProduct(null);
}
}
console.log('visible', visible);
};
// 添加购买状态监听器
const setupPurchaseListener = () => {
console.log('设置购买监听器,当前 visible 状态:', visible);
log.info('设置购买监听器', { visible });
// 如果已经有监听器,先移除
if (purchaseListenerRef.current) {
@@ -234,8 +213,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
// 创建监听器函数
const listener = (customerInfo: CustomerInfo) => {
console.log('addCustomerInfoUpdateListener:', customerInfo);
console.log('addCustomerInfoUpdateListener 触发时 visible 状态:', visible);
log.info('购买状态变化监听器触发', { customerInfo, visible });
// 检查是否有有效的购买记录
const hasActiveEntitlements = Object.keys(customerInfo.entitlements.active).length > 0;
@@ -253,7 +231,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
modalVisible: visible
});
console.log('检测到购买成功,刷新用户信息并关闭弹窗');
log.info('检测到购买成功,准备刷新用户信息并关闭弹窗');
// 延迟一点时间,确保购买流程完全完成
setTimeout(async () => {
@@ -280,16 +258,16 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
// 添加监听器
Purchases.addCustomerInfoUpdateListener(listener);
console.log('购买监听器已添加');
log.info('购买监听器已添加');
};
// 移除购买状态监听器
const removePurchaseListener = () => {
if (purchaseListenerRef.current) {
console.log('移除购买监听器');
log.info('移除购买监听器');
Purchases.removeCustomerInfoUpdateListener(purchaseListenerRef.current);
purchaseListenerRef.current = null;
console.log('购买监听器已移除');
log.info('购买监听器已移除');
}
};
@@ -302,7 +280,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
// 获取用户的购买信息
const customerInfo = await Purchases.getCustomerInfo();
console.log('用户购买信息:', customerInfo);
log.info('获取用户购买信息', { customerInfo });
// 记录详细的购买状态日志
captureMessageWithContext('获取用户购买信息成功', {
@@ -322,19 +300,19 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
Object.keys(customerInfo.entitlements.active).forEach(key => {
const entitlement = customerInfo.entitlements.active[key];
activePurchasedProductIds.push(entitlement.productIdentifier);
console.log(`激活的权益: ${key}, 产品ID: ${entitlement.productIdentifier}`);
log.debug(`激活的权益: ${key}, 产品ID: ${entitlement.productIdentifier}`);
});
// 检查非订阅购买(如终身会员)
customerInfo.nonSubscriptionTransactions.forEach(transaction => {
activePurchasedProductIds.push(transaction.productIdentifier);
console.log(`非订阅购买: ${transaction.productIdentifier}, 购买时间: ${transaction.purchaseDate}`);
log.debug(`非订阅购买: ${transaction.productIdentifier}, 购买时间: ${transaction.purchaseDate}`);
});
// 检查订阅
Object.keys(customerInfo.activeSubscriptions).forEach(productId => {
activePurchasedProductIds.push(productId);
console.log(`激活的订阅: ${productId}`);
log.debug(`激活的订阅: ${productId}`);
});
// 去重
@@ -350,18 +328,14 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
let selectedProduct: PurchasesStoreProduct | null = null;
// 优先级:终身会员 > 季度会员 > 周会员
const priorityOrder = [
'com.ilookai.mind_gpt.Lifetime',
'com.ilookai.mind_gpt.ThreeMonths',
'weekly_membership'
];
const priorityOrder = DEFAULT_PLANS.map(plan => plan.id);
// 按照优先级查找
for (const priorityProductId of priorityOrder) {
if (activePurchasedProductIds.includes(priorityProductId)) {
selectedProduct = availableProducts.find(product => product.identifier === priorityProductId) || null;
if (selectedProduct) {
console.log(`找到优先级最高的激活产品: ${priorityProductId}`);
log.info(`找到优先级最高的激活产品: ${priorityProductId}`);
break;
}
}
@@ -372,7 +346,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
for (const productId of activePurchasedProductIds) {
selectedProduct = availableProducts.find(product => product.identifier === productId) || null;
if (selectedProduct) {
console.log(`找到匹配的激活产品: ${productId}`);
log.info(`找到匹配的激活产品: ${productId}`);
break;
}
}
@@ -393,7 +367,10 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
activePurchasedProductIds,
availableProductIds: availableProducts.map(p => p.identifier)
});
console.log('用户有激活的购买记录,但没有找到匹配的可用产品');
log.warn('用户有激活的购买记录,但没有找到匹配的可用产品', {
activePurchasedProductIds,
availableProductIds: availableProducts.map(p => p.identifier)
});
}
} else {
captureMessageWithContext('用户没有激活的购买记录', {
@@ -401,16 +378,21 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
hasNonSubscriptions: customerInfo.nonSubscriptionTransactions.length > 0,
hasActiveSubscriptions: Object.keys(customerInfo.activeSubscriptions).length > 0
});
console.log('用户没有激活的购买记录,使用默认选择逻辑');
log.info('用户没有激活的购买记录,使用默认选择逻辑');
setSelectedProduct(availableProducts[0] ?? null);
}
} catch (error: any) {
console.log('检查用户购买记录失败:', error);
// 安全地处理错误对象,避免循环引用
const errorData = {
message: error?.message || '未知错误',
code: error?.code || null,
name: error?.name || 'Error',
};
log.error('检查用户购买记录失败', { error: errorData });
captureException(error);
captureMessageWithContext('检查用户购买记录失败', {
error: error.message || '未知错误',
errorCode: error.code || null
});
captureMessageWithContext('检查用户购买记录失败', errorData);
}
};
@@ -467,8 +449,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
// 执行购买
const { customerInfo, productIdentifier } = await Purchases.purchaseStoreProduct(selectedProduct);
console.log('购买成功 - customerInfo:', customerInfo);
console.log('购买成功 - productIdentifier:', productIdentifier);
log.info('购买成功', { customerInfo, productIdentifier });
// 记录购买成功事件
capturePurchaseEvent('success', `购买成功: ${productIdentifier}`, {
@@ -480,7 +461,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
});
// 购买成功后,监听器会自动处理后续逻辑(刷新用户信息、关闭弹窗等)
console.log('购买流程完成,等待监听器处理后续逻辑');
log.info('购买流程完成,等待监听器处理后续逻辑');
} catch (error: any) {
captureException(error);
@@ -527,7 +508,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
} finally {
// 确保在所有情况下都重置加载状态
setLoading(false);
console.log('购买流程结束,加载状态已重置');
log.info('购买流程结束,加载状态已重置');
}
};
@@ -545,7 +526,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
// 恢复购买
const customerInfo = await Purchases.restorePurchases();
console.log('恢复购买结果:', customerInfo);
log.info('恢复购买结果', { customerInfo });
captureMessageWithContext('恢复购买结果', {
activeEntitlementsCount: Object.keys(customerInfo.entitlements.active).length,
nonSubscriptionTransactionsCount: customerInfo.nonSubscriptionTransactions.length,
@@ -579,7 +560,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
restoredProducts.push(productId);
});
console.log('恢复的产品:', restoredProducts);
log.info('恢复的产品', { restoredProducts });
capturePurchaseEvent('restore', '恢复购买成功', {
restoredProducts,
restoredProductsCount: restoredProducts.length
@@ -599,7 +580,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
// }
// });
// console.log('后台恢复购买响应:', restoreResponse);
// log.debug('后台恢复购买响应', { restoreResponse });
// captureMessageWithContext('后台恢复购买成功', {
// responseData: restoreResponse,
@@ -620,14 +601,17 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
});
} catch (apiError: any) {
console.log('后台恢复购买接口调用失败:', apiError);
captureException(apiError);
captureMessageWithContext('后台恢复购买接口失败', {
error: apiError.message || '未知错误',
errorCode: apiError.code || null,
// 安全地处理错误对象,避免循环引用
const errorData = {
message: apiError?.message || '未知错误',
code: apiError?.code || null,
name: apiError?.name || 'Error',
restoredProductsCount: restoredProducts.length
});
};
log.error('后台恢复购买接口调用失败', { error: errorData });
captureException(apiError);
captureMessageWithContext('后台恢复购买接口失败', errorData);
// 即使后台接口失败,也显示恢复成功(因为 RevenueCat 已经确认有购买记录)
// 但不关闭弹窗,让用户知道可能需要重试
@@ -650,14 +634,18 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
});
}
} catch (error: any) {
console.log('恢复购买失败:', error);
// 安全地处理错误对象,避免循环引用
const errorData = {
message: error?.message || '未知错误',
code: error?.code || null,
name: error?.name || 'Error',
};
log.error('恢复购买失败', { error: errorData });
captureException(error);
// 记录恢复购买失败事件
capturePurchaseEvent('error', `恢复购买失败: ${error.message || '未知错误'}`, {
errorCode: error.code || null,
errorMessage: error.message || '未知错误'
});
capturePurchaseEvent('error', `恢复购买失败: ${errorData.message}`, errorData);
// 处理特定的恢复购买错误
if (error.code === 'RESTORE_CANCELLED' || error.code === 'USER_CANCELLED') {
@@ -680,39 +668,39 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
} finally {
// 确保在所有情况下都重置恢复状态
setRestoring(false);
console.log('恢复购买流程结束,恢复状态已重置');
log.info('恢复购买流程结束,恢复状态已重置');
}
};
const renderMembershipBenefits = () => (
<View style={styles.benefitsContainer}>
<Image source={require('@/assets/images/img_vip_unlock_title.png')} style={styles.benefitsTitleBg} />
<View style={styles.benefitItem}>
<MaterialIcons name="check-circle" size={20} color="#DF42D0" />
<Text style={styles.benefitText}></Text>
<View style={styles.benefitsTitleContainer}>
<Text style={styles.benefitsTitle}></Text>
<Text style={styles.benefitsSubtitle}></Text>
</View>
<View style={styles.benefitItem}>
<MaterialIcons name="check-circle" size={20} color="#DF42D0" />
<Text style={styles.benefitText}></Text>
<Text style={styles.benefitText}></Text>
</View>
<View style={styles.benefitItem}>
<MaterialIcons name="check-circle" size={20} color="#DF42D0" />
<Text style={styles.benefitText}>广</Text>
<Text style={styles.benefitText}></Text>
</View>
<View style={styles.benefitItem}>
<MaterialIcons name="check-circle" size={20} color="#DF42D0" />
<Text style={styles.benefitText}></Text>
<Text style={styles.benefitText}></Text>
</View>
{/* 皇冠图标 */}
<View style={styles.crownContainer}>
<Image
{/* <Image
source={require('@/assets/images/img_profile_vip_bg.png')}
style={styles.crownIcon}
/>
/> */}
</View>
</View>
);
@@ -725,6 +713,8 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
}
const isSelected = selectedProduct === product;
const displayTitle = product.title || plan.fallbackTitle;
const priceLabel = product.priceString || '';
return (
<TouchableOpacity
@@ -738,11 +728,11 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
disabled={loading}
activeOpacity={loading ? 1 : 0.8}
accessible={true}
accessibilityLabel={`${product.title} ${plan.price}`}
accessibilityHint={loading ? "购买进行中,无法切换套餐" : `选择${product.title}套餐`}
accessibilityLabel={`${displayTitle} ${priceLabel}`}
accessibilityHint={loading ? '购买进行中,无法切换套餐' : `选择${displayTitle}套餐`}
accessibilityState={{ disabled: loading, selected: isSelected }}
>
{product.identifier === 'com.ilookai.mind_gpt.Lifetime' && (
{plan.recommended && (
<View style={styles.recommendedBadge}>
<Text style={styles.recommendedText}></Text>
</View>
@@ -755,7 +745,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
plan.type === 'quarterly' && styles.quarterlyPlanTitle,
plan.type === 'weekly' && styles.weeklyPlanTitle,
]}>
{product.title}
{displayTitle}
</Text>
</View>
@@ -766,7 +756,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
plan.type === 'quarterly' && styles.quarterlyPlanPrice,
plan.type === 'weekly' && styles.weeklyPlanPrice,
]}>
{plan.price}
{priceLabel || '--'}
</Text>
</View>
@@ -807,6 +797,13 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
{renderMembershipBenefits()}
{/* 会员套餐选择 */}
{products.length === 0 && (
<View style={styles.configurationNotice}>
<Text style={styles.configurationText}>
RevenueCat iOS Offering
</Text>
</View>
)}
<View style={styles.plansContainer}>
{products.map(renderPlanCard)}
</View>
@@ -857,14 +854,20 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
<TouchableOpacity
style={[
styles.purchaseButton,
loading && styles.disabledButton
(loading || !selectedProduct) && styles.disabledButton
]}
onPress={handlePurchase}
disabled={loading}
disabled={loading || !selectedProduct}
accessible={true}
accessibilityLabel={loading ? "正在处理购买" : "购买会员"}
accessibilityHint={loading ? "购买正在进行中,请稍候" : "点击购买选中的会员套餐"}
accessibilityState={{ disabled: loading }}
accessibilityLabel={loading ? '正在处理购买' : '购买会员'}
accessibilityHint={
loading
? '购买正在进行中,请稍候'
: selectedProduct
? `点击购买${selectedProduct.title || '已选'}会员套餐`
: '请选择会员套餐后再进行购买'
}
accessibilityState={{ disabled: loading || !selectedProduct }}
>
{loading ? (
<View style={styles.loadingContainer}>
@@ -872,7 +875,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
<Text style={styles.purchaseButtonText}>...</Text>
</View>
) : (
<Text style={styles.purchaseButtonText}></Text>
<Text style={styles.purchaseButtonText}></Text>
)}
</TouchableOpacity>
@@ -971,6 +974,21 @@ const styles = StyleSheet.create({
marginBottom: 30,
padding: 20,
},
benefitsTitleContainer: {
alignItems: 'center',
marginBottom: 20,
},
benefitsTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#333',
marginBottom: 5,
},
benefitsSubtitle: {
fontSize: 14,
color: '#666',
textAlign: 'center',
},
benefitsTitleBg: {
width: 180,
height: 20,
@@ -1173,4 +1191,4 @@ const styles = StyleSheet.create({
lineHeight: 18,
textAlign: 'center',
},
});
});