feat: implement user balance management with hooks and store
This commit is contained in:
@@ -3,26 +3,16 @@ import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
Pressable,
|
||||
useWindowDimensions,
|
||||
} from 'react-native'
|
||||
import { LinearGradient } from 'expo-linear-gradient'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import BottomSheet, { BottomSheetView, BottomSheetBackdrop, BottomSheetScrollView } from '@gorhom/bottom-sheet'
|
||||
import BottomSheet, { BottomSheetView, BottomSheetBackdrop } from '@gorhom/bottom-sheet'
|
||||
import { CloseIcon } from '@/components/icon'
|
||||
import TopUpDrawer, { TopUpOption } from '@/components/drawer/TopUpDrawer'
|
||||
|
||||
export type PointsTabType = 'all' | 'consume' | 'obtain'
|
||||
|
||||
export interface PointsTransaction {
|
||||
id: string
|
||||
title: string
|
||||
date: string
|
||||
points: number // 正数表示获得,负数表示消耗
|
||||
}
|
||||
|
||||
export interface PointsDrawerProps {
|
||||
/**
|
||||
* 是否显示抽屉
|
||||
@@ -44,29 +34,22 @@ export interface PointsDrawerProps {
|
||||
* 额外充值积分
|
||||
*/
|
||||
topUpPoints?: number
|
||||
/**
|
||||
* 交易记录列表
|
||||
*/
|
||||
transactions?: PointsTransaction[]
|
||||
}
|
||||
|
||||
export default function PointsDrawer({
|
||||
visible,
|
||||
onClose,
|
||||
totalPoints = 60,
|
||||
totalPoints = 0,
|
||||
subscriptionPoints = 0,
|
||||
topUpPoints = 0,
|
||||
transactions = [],
|
||||
}: PointsDrawerProps) {
|
||||
const { t } = useTranslation()
|
||||
const { height: screenHeight } = useWindowDimensions()
|
||||
const insets = useSafeAreaInsets()
|
||||
const bottomSheetRef = useRef<BottomSheet>(null)
|
||||
const [pointsTab, setPointsTab] = useState<PointsTabType>('all')
|
||||
const [topUpDrawerVisible, setTopUpDrawerVisible] = useState(false)
|
||||
|
||||
const snapPoints = useMemo(() => [screenHeight * 0.85], [screenHeight])
|
||||
|
||||
|
||||
const snapPoints = useMemo(() => [380], [])
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
bottomSheetRef.current?.expand()
|
||||
@@ -74,13 +57,13 @@ export default function PointsDrawer({
|
||||
bottomSheetRef.current?.close()
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
|
||||
const handleSheetChanges = useCallback((index: number) => {
|
||||
if (index === -1) {
|
||||
onClose()
|
||||
}
|
||||
}, [onClose])
|
||||
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: any) => (
|
||||
<BottomSheetBackdrop
|
||||
@@ -93,40 +76,6 @@ export default function PointsDrawer({
|
||||
[]
|
||||
)
|
||||
|
||||
// 标签页配置
|
||||
const tabOptions: Array<{ value: PointsTabType; label: string }> = [
|
||||
{ value: 'all', label: t('pointsDrawer.all') },
|
||||
{ value: 'consume', label: t('pointsDrawer.consume') },
|
||||
{ value: 'obtain', label: t('pointsDrawer.obtain') },
|
||||
]
|
||||
|
||||
// 根据标签页过滤交易记录
|
||||
const filteredTransactions = transactions.filter((transaction) => {
|
||||
if (pointsTab === 'all') return true
|
||||
if (pointsTab === 'consume') return transaction.points < 0
|
||||
if (pointsTab === 'obtain') return transaction.points > 0
|
||||
return true
|
||||
})
|
||||
|
||||
// 如果没有提供交易记录,使用示例数据
|
||||
const displayTransactions =
|
||||
filteredTransactions.length > 0
|
||||
? filteredTransactions
|
||||
: [
|
||||
{
|
||||
id: '1',
|
||||
title: t('pointsDrawer.dailyFreePoints'),
|
||||
date: '2025年11月28日 10:33',
|
||||
points: 60,
|
||||
},
|
||||
...Array.from({ length: 60 }, (_, i) => ({
|
||||
id: `example-${i + 2}`,
|
||||
title: t('pointsDrawer.dailyFreePoints'),
|
||||
date: '2025年11月28日 10:33',
|
||||
points: -60,
|
||||
})),
|
||||
]
|
||||
|
||||
return (
|
||||
<BottomSheet
|
||||
ref={bottomSheetRef}
|
||||
@@ -150,75 +99,24 @@ export default function PointsDrawer({
|
||||
<CloseIcon />
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title}>{t('pointsDrawer.title')}</Text>
|
||||
{/* 积分总额 */}
|
||||
<View style={styles.balance}>
|
||||
<Text style={styles.balanceValue}>{totalPoints}</Text>
|
||||
</View>
|
||||
|
||||
{/* 积分类型细分 */}
|
||||
<View style={styles.breakdown}>
|
||||
<Text style={styles.breakdownText}>{t('pointsDrawer.subscriptionPoints')}
|
||||
<Text style={styles.breakdownTextValue}>{subscriptionPoints}</Text>
|
||||
</Text>
|
||||
<View style={styles.breakdownTextSeparator} />
|
||||
<Text style={styles.breakdownText}>{t('pointsDrawer.topUpPoints')}
|
||||
<Text style={styles.breakdownTextValue}>{topUpPoints}</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 标签页 */}
|
||||
<View style={styles.tabs}>
|
||||
{tabOptions.map((tab) => {
|
||||
const isActive = pointsTab === tab.value
|
||||
return (
|
||||
<Pressable
|
||||
key={tab.value}
|
||||
style={[styles.tab]}
|
||||
onPress={() => setPointsTab(tab.value)}
|
||||
>
|
||||
<View style={styles.tabContent}>
|
||||
{isActive && (
|
||||
<LinearGradient
|
||||
colors={['#FF9966', '#FF6699', '#9966FF']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={styles.tabGradient}
|
||||
/>
|
||||
)}
|
||||
<Text style={[styles.tabText, isActive && styles.tabTextActive]}>{tab.label}</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
})}
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title}>{t('pointsDrawer.title')}</Text>
|
||||
{/* 积分总额 */}
|
||||
<View style={styles.balance}>
|
||||
<Text style={styles.balanceValue}>{totalPoints}</Text>
|
||||
</View>
|
||||
|
||||
{/* 交易历史列表 */}
|
||||
<BottomSheetScrollView
|
||||
style={styles.list}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{displayTransactions.map((transaction) => (
|
||||
<View key={transaction.id} style={styles.item}>
|
||||
<View style={styles.itemLeft}>
|
||||
<Text style={styles.itemTitle}>{transaction.title}</Text>
|
||||
<Text style={styles.itemDate}>{transaction.date}</Text>
|
||||
</View>
|
||||
<Text
|
||||
style={[
|
||||
styles.itemPoints,
|
||||
transaction.points < 0 && styles.itemPointsNegative,
|
||||
]}
|
||||
>
|
||||
{transaction.points > 0 ? '+' : ''}
|
||||
{transaction.points}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</BottomSheetScrollView>
|
||||
{/* 积分类型细分 */}
|
||||
<View style={styles.breakdown}>
|
||||
<Text style={styles.breakdownText}>{t('pointsDrawer.subscriptionPoints')}
|
||||
<Text style={styles.breakdownTextValue}>{subscriptionPoints}</Text>
|
||||
</Text>
|
||||
<View style={styles.breakdownTextSeparator} />
|
||||
<Text style={styles.breakdownText}>{t('pointsDrawer.topUpPoints')}
|
||||
<Text style={styles.breakdownTextValue}>{topUpPoints}</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<View style={[styles.footer, { paddingBottom: Math.max(insets.bottom, 16) }]}>
|
||||
@@ -233,7 +131,6 @@ export default function PointsDrawer({
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={styles.subscribeButtonGradient}
|
||||
|
||||
>
|
||||
<Text style={styles.subscribeButtonText}>{t('pointsDrawer.subscribeForPoints')}</Text>
|
||||
</LinearGradient>
|
||||
@@ -248,7 +145,7 @@ export default function PointsDrawer({
|
||||
</Pressable>
|
||||
</View>
|
||||
</BottomSheetView>
|
||||
|
||||
|
||||
{/* 充值抽屉 */}
|
||||
<TopUpDrawer
|
||||
visible={topUpDrawerVisible}
|
||||
@@ -276,9 +173,6 @@ const styles = StyleSheet.create({
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
},
|
||||
handleIndicator: {
|
||||
backgroundColor: '#666666',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#090A0B',
|
||||
@@ -296,8 +190,6 @@ const styles = StyleSheet.create({
|
||||
titleContainer: {
|
||||
paddingLeft: 20,
|
||||
marginTop: -4,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#3A3A3A',
|
||||
},
|
||||
title: {
|
||||
color: '#F5F5F5',
|
||||
@@ -340,77 +232,6 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: '#3A3A3A',
|
||||
marginTop: 2,
|
||||
},
|
||||
tabs: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
marginTop:20,
|
||||
marginBottom:24,
|
||||
},
|
||||
tab: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
tabContent: {
|
||||
position: 'relative',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
tabGradient: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: 10,
|
||||
backgroundColor: '#FF9966',
|
||||
},
|
||||
tabText: {
|
||||
color: '#ABABAB',
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
},
|
||||
tabTextActive: {
|
||||
color: '#F5F5F5',
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
},
|
||||
list: {
|
||||
height: 400,
|
||||
},
|
||||
listContent: {
|
||||
paddingBottom: 16,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
item: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#1C1E20',
|
||||
},
|
||||
itemLeft: {
|
||||
flex: 1,
|
||||
},
|
||||
itemTitle: {
|
||||
color: '#F5F5F5',
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
marginBottom: 4,
|
||||
},
|
||||
itemDate: {
|
||||
color: '#ABABAB',
|
||||
fontSize: 12,
|
||||
fontWeight: '400',
|
||||
},
|
||||
itemPoints: {
|
||||
color: '#4CAF50',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
itemPointsNegative: {
|
||||
color: '#F5F5F5',
|
||||
},
|
||||
footer: {
|
||||
paddingTop: 20,
|
||||
paddingHorizontal: 16,
|
||||
@@ -445,4 +266,3 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '400',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -4,14 +4,27 @@ import {
|
||||
Text,
|
||||
StyleSheet,
|
||||
Pressable,
|
||||
useWindowDimensions,
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
} from 'react-native'
|
||||
import { LinearGradient } from 'expo-linear-gradient'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { useRouter } from 'expo-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import BottomSheet, { BottomSheetView, BottomSheetBackdrop } from '@gorhom/bottom-sheet'
|
||||
import { root } from '@repo/core'
|
||||
import { AlipayController } from '@repo/sdk'
|
||||
import { CloseIcon, CheckIcon, UncheckedIcon, PointsIcon } from '@/components/icon'
|
||||
import Toast from '@/components/ui/Toast'
|
||||
import { useUserBalanceStore } from '@/stores/userBalanceStore'
|
||||
|
||||
// 动态导入支付宝 SDK(需要安装 expo-native-alipay)
|
||||
let Alipay: any = null
|
||||
try {
|
||||
Alipay = require('expo-native-alipay').default
|
||||
} catch (e) {
|
||||
console.warn('expo-native-alipay not installed, payment will not work')
|
||||
}
|
||||
|
||||
export interface TopUpOption {
|
||||
id: string
|
||||
@@ -65,6 +78,9 @@ const defaultOptions: TopUpOption[] = [
|
||||
{ id: '4', points: 10000, price: 20 },
|
||||
]
|
||||
|
||||
// 支付宝回调 scheme(需要与 app.json 中配置一致)
|
||||
const ALIPAY_SCHEME = 'popcore'
|
||||
|
||||
export default function TopUpDrawer({
|
||||
visible,
|
||||
onClose,
|
||||
@@ -82,6 +98,10 @@ export default function TopUpDrawer({
|
||||
options[0] || null
|
||||
)
|
||||
const [agreed, setAgreed] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// 获取余额 store 的方法
|
||||
const { load: loadBalance, restartPolling } = useUserBalanceStore()
|
||||
|
||||
const snapPoints = useMemo(() => [420], [])
|
||||
|
||||
@@ -119,9 +139,66 @@ export default function TopUpDrawer({
|
||||
// 如果没有传入标题,使用默认翻译
|
||||
const displayTitle = topUpTitle || t('topUp.title')
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (selectedOption && agreed) {
|
||||
onConfirm?.(selectedOption)
|
||||
// 初始化支付宝 SDK
|
||||
useEffect(() => {
|
||||
if (Alipay && Platform.OS === 'ios') {
|
||||
Alipay.setAlipayScheme(ALIPAY_SCHEME)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!selectedOption || !agreed) return
|
||||
|
||||
// 检查支付宝 SDK 是否可用
|
||||
if (!Alipay) {
|
||||
Toast.show(t('topUp.alipayNotInstalled') || '支付宝 SDK 未安装')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
// 1. 调用后端 API 创建订单
|
||||
const alipay = root.get(AlipayController)
|
||||
const response = await alipay.preRecharge({
|
||||
credits: selectedOption.points,
|
||||
})
|
||||
|
||||
if (!response?.orderStr) {
|
||||
Toast.show(t('topUp.createOrderFailed') || '创建订单失败')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 调用支付宝 SDK 发起支付
|
||||
const result = await Alipay.pay(response.orderStr)
|
||||
console.log('Alipay payment result:', result)
|
||||
|
||||
// 3. 处理支付结果
|
||||
if (result.resultStatus === '9000') {
|
||||
// 支付成功
|
||||
Toast.show(t('topUp.paymentSuccess') || '支付成功!积分正在到账中...')
|
||||
// 刷新余额
|
||||
loadBalance(true)
|
||||
restartPolling()
|
||||
// 关闭抽屉
|
||||
onClose()
|
||||
// 调用外部回调
|
||||
onConfirm?.(selectedOption)
|
||||
} else if (result.resultStatus === '6001') {
|
||||
// 用户取消
|
||||
Toast.show(t('topUp.paymentCancelled') || '支付已取消')
|
||||
} else {
|
||||
// 支付失败
|
||||
Toast.show(t('topUp.paymentFailed') || '支付失败,请重试')
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Payment error:', err)
|
||||
Toast.show(err?.message || t('topUp.paymentError') || '支付出错')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
// 无论成功失败都刷新余额
|
||||
loadBalance(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +290,7 @@ export default function TopUpDrawer({
|
||||
<Pressable
|
||||
style={styles.confirmButton}
|
||||
onPress={handleConfirm}
|
||||
disabled={!agreed || !selectedOption}
|
||||
disabled={!agreed || !selectedOption || loading}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={['#FF9966', '#FF6699', '#9966FF']}
|
||||
@@ -221,10 +298,14 @@ export default function TopUpDrawer({
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={[
|
||||
styles.confirmButtonGradient,
|
||||
(!agreed || !selectedOption) && styles.confirmButtonDisabled,
|
||||
(!agreed || !selectedOption || loading) && styles.confirmButtonDisabled,
|
||||
]}
|
||||
>
|
||||
<Text style={styles.confirmButtonText}>{t('topUp.confirm')}</Text>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#F5F5F5" size="small" />
|
||||
) : (
|
||||
<Text style={styles.confirmButtonText}>{t('topUp.confirm')}</Text>
|
||||
)}
|
||||
</LinearGradient>
|
||||
</Pressable>
|
||||
<View style={styles.agreementContainer}>
|
||||
|
||||
Reference in New Issue
Block a user