expo-duooomi-app/app/pointList.native.tsx

326 lines
12 KiB
TypeScript
Raw Permalink 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.

import { Ionicons } from '@expo/vector-icons'
import { root } from '@repo/core'
import { AlipayController } from '@repo/sdk'
import { LinearGradient } from 'expo-linear-gradient'
import Alipay from 'expo-native-alipay'
import { Stack, useRouter } from 'expo-router'
import ExpoWeChat from 'expo-wechat'
import { observer } from 'mobx-react-lite'
import React, { useEffect, useState } from 'react'
import { Dimensions, Platform, RefreshControl, ScrollView } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { Block, Img, Text, Toast } from '@/@share/components'
import { IOS_UNIVERSAL_LINK, SCHEME } from '@/app.config'
import { handleError } from '@/hooks/data/use-error'
import { useIOSPurchase } from '@/hooks/use-ios-purchase'
import { userBalanceStore } from '@/stores'
import { openUrl } from '@/utils'
import { cn } from '@/utils/cn'
const { width: SCREEN_WIDTH } = Dimensions.get('window')
const OPTION_WIDTH = (SCREEN_WIDTH - 32 - 15) / 2 // 32px padding, 15px gap
const RECHARGE_OPTIONS = [
{ id: 'coin_500', points: '500', price: '¥70' },
{ id: 'coin_1000', points: '1,000', price: '¥140' },
{ id: 'coin_2500', points: '2,500', price: '¥350' },
{ id: 'coin_5000', points: '5,000', price: '¥700' },
]
const skus = RECHARGE_OPTIONS.map((option) => option.id)
const PAYMENT_METHODS = [
{ id: 'alipay', label: '支付宝' },
{ id: 'applePay', label: '苹果支付' },
// { id: 'wechat', label: '微信' },
] as const
const isIos = Platform.OS === 'ios'
const ChargePage = observer(function ChargePage() {
const router = useRouter()
const insets = useSafeAreaInsets()
const [selectedOption, setSelectedOption] = useState(RECHARGE_OPTIONS[0].id)
const [paymentMethod, setPaymentMethod] = useState<'alipay' | 'wechat' | 'applePay'>(() => {
if (isIos) {
return 'applePay'
}
return 'alipay'
})
const [refreshing, setRefreshing] = useState(false)
const [payList, setPayList] = useState<any[]>(() => {
if (isIos) {
return PAYMENT_METHODS.filter((method) => method.id === 'applePay')
}
return PAYMENT_METHODS.filter((method) => method.id === 'alipay')
})
// 使用MobX Store中的余额信息
const { balance } = userBalanceStore
const selectedRecharge = RECHARGE_OPTIONS.find((option) => option.id === selectedOption)
// iOS 专用 IAP Hook
const { connected, products, requestPurchase } = useIOSPurchase({
skus,
onPurchaseSuccess: async (purchase) => {
// TODO: 验证购买凭证
console.log('💰 Purchase completed:', purchase.productId)
// 刷新用户余额
await userBalanceStore.load(true)
Toast.show({ title: '购买成功!积分已到账' })
},
onProductsFetched: (products) => {
console.log(
'📦 Available products:',
products.map((p) => p.productId),
)
},
})
const handleApplePay = async (productId: string) => {
await requestPurchase(productId)
}
const onRefresh = async () => {
setRefreshing(true)
await userBalanceStore.load(true) // 强制刷新余额
setRefreshing(false)
}
const initPay = async () => {
// 设置支付宝回调 URL Scheme使用 App 的统一 scheme
if (Platform.OS === 'ios') {
Alipay.setAlipayScheme(SCHEME)
} else {
// Alipay.setAlipaySandbox(true) // 开启沙箱模式,测试环境使用
}
console.log('-------alipay version:', await Alipay.getVersion())
const wechatAppId = 'wx940e1ed91a5c303c'
const result = await ExpoWeChat.registerApp(wechatAppId, IOS_UNIVERSAL_LINK)
console.log('-------wechat', result)
}
const handlePay = async () => {
// Toast.show({ title: '支付宝支付暂未开放' })
// return
// 支付宝端支付
// payInfo 是后台拼接好的支付参数
// return_url=
// const payInfo =
// 'alipay_sdk=alipay-sdk-java-dynamicVersionNo&app_id=2021001172656340&biz_content=%7B%22out_trade_no%22%3A%221111112222222%22%2C%22total_amount%22%3A%220.01%22%2C%22subject%22%3A%221234%22%2C%22product_code%22%3A%22QUICK_MSECURITY_PAY%22%7D&charset=UTF-8&format=json&method=alipay.trade.app.pay&notify_url=http%3A%2F%2Fane.boshu.ltd%2Fowner%2Fpay%2Fapi%2FownerPay%2Fcallback&sign=oUQmGtkv8mrhJ0YwHl9%2FfxMcoLACWuSFKiMTC4Id8nc%2FZVvDQ6MLQq5hhtEN03Qn1%2BAtzTAaofE8nNixdroxOek2l5YtOAcYcXVYlJIyogN%2B22erN2NpDTWJ7tQTKgYFDJLRiG0DZJaxfADhUUF6UR9kdA8omoXKLDlP17ZPUs5Jr4aKv5HJtH5C53ui7PbmyWYg934L4UDC2F%2F9pPQlRwwDeE1SAaV3HW9Dt83kK52o8%2FlChXdotbFdAvH0d4qYGhpEYU5sepj9xiOMyL9aC4pMXW9INYLLGbvtqtlRchZTAfH5yji6nqqQm9KKMmcVrWdBDLyjFVNpejq1UjbJBw%3D%3D&sign_type=RSA2&timestamp=2020-07-09+12%3A16%3A16&version=1.0'
if (paymentMethod === 'alipay') {
if (!selectedRecharge?.points) {
Toast.show({ title: '请选择充值选项' })
return
}
const alipay = root.get(AlipayController)
const { data, error } = await handleError(
async () =>
await alipay.preRecharge({
// credits: 2,
credits: parseInt(selectedRecharge?.points.replace(/,/g, '')),
}),
)
// console.log('error----------', error)
// console.log('data-----------', data)
if (error || !data?.orderStr) {
Toast.show({ title: error?.message || '创建订单失败' })
return
}
const resule = await Alipay.pay(data.orderStr)
console.log('alipay:resule-->>>', resule)
// 支付完成后显示积分到账loading
if (resule.resultStatus === '9000') {
// 支付成功,立即刷新余额并重启轮询
userBalanceStore.load(true)
userBalanceStore.restartPolling() // 重启轮询以更频繁地检查余额变化
Toast.show({ title: '支付成功!积分正在到账中...' })
} else {
Toast.show({ title: '支付取消或失败' })
userBalanceStore.load(true) // 刷新余额状态
}
} else if (paymentMethod === 'applePay') {
handleApplePay(selectedRecharge?.id!)
}
}
const openWeb = () => {
// webview 显示异常
// router.push({
// pathname: '/webview',
// params: {
// // url: 'http://baidu.com',
// url: 'https://mixvideo.bowong.cc/terms',
// title: '付费服务协议',
// },
// })
openUrl('https://mixvideo.bowong.cc/terms', '付费服务协议')
}
useEffect(() => {
userBalanceStore.load(true) // 页面加载时获取余额
initPay()
}, [])
const renderHeader = () => (
<Block className="flex-row items-center justify-between px-[16px]" style={{ paddingTop: 12, paddingBottom: 12 }}>
<Block className="ml-[-8px] size-[40px] items-center justify-center" opacity={0.7} onClick={() => router.back()}>
<Ionicons color="white" name="chevron-back" size={24} />
</Block>
<Text className="text-[16px] font-[700] text-white"></Text>
<Block className="w-[32px]" />
</Block>
)
const renderMyPoints = () => (
<Block className="mt-[12px] items-center justify-center py-[24px]">
<Text className="text-[14px] font-[400] text-[#B0B0B0]"></Text>
<Text className="mt-[4px] text-[40px] font-[900] tracking-tighter text-fg">{balance}</Text>
</Block>
)
const renderOptions = () => (
<Block className="mt-[16px] px-[16px]">
<Text className="mb-[16px] text-[14px] font-[700] text-white"></Text>
<Block className="flex-row flex-wrap justify-between gap-y-[16px]">
{RECHARGE_OPTIONS.map((item) => {
const isSelected = selectedOption === item.id
return (
<Block
key={item.id}
opacity={0.9}
style={{ width: OPTION_WIDTH }}
className={cn(
'relative items-center justify-center overflow-hidden rounded-[16px] border-[2px] bg-[#FFFFFF33] py-[12px]',
isSelected ? 'border-[#FFE500]' : 'border-transparent',
)}
onClick={() => setSelectedOption(item.id)}
>
<Img
className="absolute inset-0"
source={require('@/assets/images/itemBg.png')}
style={{ resizeMode: 'cover' }}
/>
{/* Flash Icon */}
<Block className="mb-[8px]">
<Ionicons color="#FFD700" name="flash" size={24} />
</Block>
{/* Points */}
<Text className="mb-[16px] text-[24px] font-[700] text-white">{item.points}</Text>
{/* Price Button */}
<Block className="border-[#000000]">
<LinearGradient
colors={['#393939', '#060606']}
end={{ x: 0, y: 1 }}
start={{ x: 0, y: 0 }}
style={{
borderRadius: 100,
width: 120,
height: 32,
alignItems: 'center',
justifyContent: 'center',
borderColor: '#000000',
overflow: 'hidden',
borderWidth: 1,
}}
>
<Text className="text-[12px] font-[600] text-white"> {item.price}</Text>
</LinearGradient>
</Block>
</Block>
)
})}
</Block>
</Block>
)
const renderPaymentMethods = () => (
<Block className="mt-[32px] px-[16px]">
<Text className="font-700 mb-[16px] text-[14px] text-white">:</Text>
<Block className="gap-[12px]">
{payList.map((method) => {
const isSelected = paymentMethod === method.id
return (
<Block
key={method.id}
opacity={0.8}
className={cn(
'flex-row items-center gap-[12px] rounded-full px-[20px] py-[16px]',
isSelected ? 'bg-white' : 'bg-[#FFFFFF33]',
)}
onClick={() => setPaymentMethod(method.id)}
>
<Block
className={cn(
'h-[20px] w-[20px] items-center justify-center rounded-full',
isSelected ? 'bg-black' : 'border-[2px] border-white/50',
)}
>
{isSelected && <Ionicons color="white" name="checkmark" size={14} />}
</Block>
<Text className={cn('font-700 text-[16px]', isSelected ? 'text-black' : 'text-white')}>
{method.label}
</Text>
</Block>
)
})}
</Block>
</Block>
)
const renderBottomBar = () => (
<Block
className="absolute inset-x-0 bottom-0 border-t border-white/5 bg-[#1C1E22]/95 px-[16px]"
style={{ paddingBottom: insets.bottom + 12, paddingTop: 16 }}
>
<Block
className="h-[52px] items-center justify-center rounded-full bg-accent shadow-lg shadow-[#FFE500]/20"
opacity={0.8}
onClick={handlePay}
>
<Text className="text-[16px] font-[900] text-black"></Text>
</Block>
<Block className="mt-[12px] flex-row justify-center">
<Text className="text-[11px] text-[#888888]"> </Text>
<Block opacity={0.6} onClick={openWeb}>
<Text className="text-[11px] text-accent underline"></Text>
</Block>
</Block>
</Block>
)
return (
<Block className="flex-1 bg-[#21221D]">
<Stack.Screen options={{ headerShown: false }} />
{renderHeader()}
<ScrollView
contentContainerStyle={{ flexGrow: 1, paddingBottom: 200 }}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor="#FFE500" colors={['#FFE500']} />
}
>
{renderMyPoints()}
{renderOptions()}
{renderPaymentMethods()}
</ScrollView>
{renderBottomBar()}
</Block>
)
})
export default ChargePage