| @@ -0,0 +1,99 @@ | |||
| 1 | +import React, { useState } from 'react'; | ||
| 2 | +import { View, Text, TouchableOpacity, Modal, FlatList, StyleSheet } from 'react-native'; | ||
| 3 | + | ||
| 4 | +type DropdownItem = { id: string | number; type: string }; | ||
| 5 | + | ||
| 6 | +type CustomDropdownProps = { | ||
| 7 | + data: DropdownItem[]; | ||
| 8 | + onSelect: (item: DropdownItem) => void; | ||
| 9 | + placeholder?: string; | ||
| 10 | +}; | ||
| 11 | + | ||
| 12 | +export const CustomDropdown: React.FC<CustomDropdownProps> = ({ data, onSelect, placeholder = 'Select...' }): JSX.Element => { | ||
| 13 | + const [visible, setVisible] = useState<boolean>(false); | ||
| 14 | + const [selected, setSelected] = useState<DropdownItem | null>(null); | ||
| 15 | + | ||
| 16 | + const handleSelect = (item: DropdownItem) => { | ||
| 17 | + setSelected(item); | ||
| 18 | + onSelect(item); | ||
| 19 | + setVisible(false); | ||
| 20 | + }; | ||
| 21 | + | ||
| 22 | + return ( | ||
| 23 | + <View style={styles.container}> | ||
| 24 | + <TouchableOpacity style={styles.dropdownButton} onPress={() => setVisible(true)}> | ||
| 25 | + <Text style={styles.buttonText}>{selected ? `${'类型: ' + selected.type + ' id: ' + selected.id}` : placeholder}</Text> | ||
| 26 | + <Text style={styles.arrow}>▼</Text> | ||
| 27 | + </TouchableOpacity> | ||
| 28 | + | ||
| 29 | + <Modal transparent={true} visible={visible} onRequestClose={() => setVisible(false)}> | ||
| 30 | + <TouchableOpacity style={styles.modalOverlay} onPress={() => setVisible(false)}> | ||
| 31 | + <View style={styles.modalContent}> | ||
| 32 | + <FlatList | ||
| 33 | + data={data} | ||
| 34 | + keyExtractor={(item) => String(item.id)} | ||
| 35 | + renderItem={({ item }) => ( | ||
| 36 | + <TouchableOpacity style={styles.option} onPress={() => handleSelect(item)}> | ||
| 37 | + <Text style={styles.optionText}>{'类型: ' + item.type} {'id: ' + item.id}</Text> | ||
| 38 | + </TouchableOpacity> | ||
| 39 | + )} | ||
| 40 | + /> | ||
| 41 | + </View> | ||
| 42 | + </TouchableOpacity> | ||
| 43 | + </Modal> | ||
| 44 | + </View> | ||
| 45 | + ); | ||
| 46 | +}; | ||
| 47 | + | ||
| 48 | +const styles = StyleSheet.create({ | ||
| 49 | + container: { | ||
| 50 | + margin: 10, | ||
| 51 | + }, | ||
| 52 | + | ||
| 53 | + dropdownButton: { | ||
| 54 | + flexDirection: 'row', | ||
| 55 | + justifyContent: 'space-between', | ||
| 56 | + alignItems: 'center', | ||
| 57 | + padding: 10, | ||
| 58 | + borderWidth: 1, | ||
| 59 | + borderColor: '#666', | ||
| 60 | + borderRadius: 6, | ||
| 61 | + backgroundColor: '#fff', | ||
| 62 | + marginVertical: 4, | ||
| 63 | + }, | ||
| 64 | + | ||
| 65 | + buttonText: { | ||
| 66 | + fontSize: 14, | ||
| 67 | + color: '#110b0bff', | ||
| 68 | + }, | ||
| 69 | + | ||
| 70 | + arrow: { | ||
| 71 | + fontSize: 12, | ||
| 72 | + color: '#0a0a0aff', | ||
| 73 | + }, | ||
| 74 | + | ||
| 75 | + modalOverlay: { | ||
| 76 | + flex: 1, | ||
| 77 | + backgroundColor: '#333', | ||
| 78 | + justifyContent: 'center', | ||
| 79 | + alignItems: 'center', | ||
| 80 | + }, | ||
| 81 | + | ||
| 82 | + modalContent: { | ||
| 83 | + backgroundColor: '#f0ececff', | ||
| 84 | + borderRadius: 8, | ||
| 85 | + padding: 10, | ||
| 86 | + width: '80%', | ||
| 87 | + maxHeight: 300, | ||
| 88 | + }, | ||
| 89 | + | ||
| 90 | + option: { | ||
| 91 | + padding: 15, | ||
| 92 | + borderBottomWidth: 1, | ||
| 93 | + borderBottomColor: '#070606ff', | ||
| 94 | + }, | ||
| 95 | + | ||
| 96 | + optionText: { | ||
| 97 | + fontSize: 16, | ||
| 98 | + }, | ||
| 99 | +}); | ||
| @@ -0,0 +1,861 @@ | |||
| 1 | +import React from 'react'; | ||
| 2 | +import { | ||
| 3 | + View, | ||
| 4 | + Text, | ||
| 5 | + StyleSheet, | ||
| 6 | + Button, | ||
| 7 | + ScrollView, | ||
| 8 | + Alert, | ||
| 9 | + EmitterSubscription | ||
| 10 | +} from 'react-native'; | ||
| 11 | +import { | ||
| 12 | + finishTransaction, | ||
| 13 | + initConnection, | ||
| 14 | + endConnection, | ||
| 15 | + getProducts, | ||
| 16 | + getSubscriptions, | ||
| 17 | + getAvailablePurchases, | ||
| 18 | + requestPurchase, | ||
| 19 | + requestSubscription, | ||
| 20 | + getPurchaseHistory, | ||
| 21 | + getAppTransaction, | ||
| 22 | + deepLinkToSubscriptions, | ||
| 23 | + purchaseUpdatedListener, | ||
| 24 | + purchaseErrorListener, | ||
| 25 | + Purchase, | ||
| 26 | + PurchaseError, | ||
| 27 | + Product | ||
| 28 | +} from 'react-native-iap'; | ||
| 29 | +import { CustomDropdown } from './CustomFropdown'; | ||
| 30 | +import { Tester, TestCase, TestSuite } from '@rnoh/testerino'; | ||
| 31 | +import TestPage from "./test" | ||
| 32 | + | ||
| 33 | +class AppDemo extends React.Component { | ||
| 34 | + purchaseUpdatedSubscription: EmitterSubscription | null; | ||
| 35 | + purchaseErrorSubscription: EmitterSubscription | null; | ||
| 36 | + state = { | ||
| 37 | + log: '', | ||
| 38 | + connected: false, | ||
| 39 | + packageName: '', | ||
| 40 | + productType: 'inapp', | ||
| 41 | + skuIds: 'sku_001,sku_002', | ||
| 42 | + purchaseToken: '', | ||
| 43 | + | ||
| 44 | + productList: [] as Product[], | ||
| 45 | + /** 刷新失败购买缓存结果:null=未点击,true=成功,false=失败(用于对应按钮下方渲染) */ | ||
| 46 | + flushPendingResult: null as boolean | null, | ||
| 47 | + /** 按类型获取商品是否请求中(用于对应按钮旁渲染) */ | ||
| 48 | + getItemsByTypeLoading: false, | ||
| 49 | + /** 开始监听结果:null=未点击,'ok'=成功,'fail'=失败 */ | ||
| 50 | + UpdatedlisteningStatus: null as string | null, | ||
| 51 | + ErrorlisteningStatus: null as string | null, | ||
| 52 | + /** 未完成订单列表(用于完成交易功能) */ | ||
| 53 | + availablePurchases: [] as ReadonlyArray<Record<string, unknown>>, | ||
| 54 | + /** 查询未完成订单是否请求中 */ | ||
| 55 | + availablePurchasesLoading: false, | ||
| 56 | + /** 完成交易是否请求中 */ | ||
| 57 | + finishTransactionLoading: false, | ||
| 58 | + /** 购买历史记录 */ | ||
| 59 | + purchaseHistory: [] as ReadonlyArray<Record<string, unknown>>, | ||
| 60 | + /** 应用交易信息 */ | ||
| 61 | + appTransaction: null as Record<string, unknown> | null, | ||
| 62 | + /** 订阅管理页面跳转 SKU */ | ||
| 63 | + subscriptionSku: 'SubProduct01001', | ||
| 64 | + list: [ | ||
| 65 | + { id: 'SubProduct01002', type: 'inapp' }, | ||
| 66 | + { id: 'ohos_consume_002', type: 'inapp' }, | ||
| 67 | + { id: 'SubProduct01001', type: 'subs' }, | ||
| 68 | + { id: 'ohos_consume_001', type: 'inapp' }, | ||
| 69 | + ], | ||
| 70 | + showTestPage: false | ||
| 71 | + }; | ||
| 72 | + /** 从商品列表选中一项用于购买:填入 id、类型并直接发起购买 */ | ||
| 73 | + selectProductForPurchase = (productId: string, type?: string) => { | ||
| 74 | + const skuIds = String(productId).trim(); | ||
| 75 | + const productType = | ||
| 76 | + type != null && String(type).trim() | ||
| 77 | + ? String(type).trim() | ||
| 78 | + : this.state.productType; | ||
| 79 | + this.setState({ skuIds, productType }, () => this.buyItemByType()); | ||
| 80 | + console.log(`已选中并发起购买: ${productId} (${productType})`); | ||
| 81 | + }; | ||
| 82 | + | ||
| 83 | + appendLog = (msg: string) => { | ||
| 84 | + this.setState((prev: { log: string }) => ({ | ||
| 85 | + log: prev.log + `\n ${msg}`, | ||
| 86 | + })); | ||
| 87 | + }; | ||
| 88 | + | ||
| 89 | + // 初始化 IAP 连接(对应 IAP Kit:createInstance + queryEnvironmentStatus) | ||
| 90 | + initConnection = async () => { | ||
| 91 | + try { | ||
| 92 | + const ok = await initConnection(); | ||
| 93 | + this.setState({ connected: ok }); | ||
| 94 | + console.log(`initConnection: ${ok ? '成功' : '失败'}`); | ||
| 95 | + } catch (e) { | ||
| 96 | + console.log(`initConnection 异常: ${String(e)}`); | ||
| 97 | + } | ||
| 98 | + }; | ||
| 99 | + | ||
| 100 | + // 结束 IAP 连接(释放 IapInstance,不再调用 IAP 接口) | ||
| 101 | + endConnection = async () => { | ||
| 102 | + try { | ||
| 103 | + const ok = await endConnection(); | ||
| 104 | + this.setState({ connected: false }); | ||
| 105 | + console.log(`endConnection: ${ok ? '成功' : '失败'}`); | ||
| 106 | + } catch (e) { | ||
| 107 | + console.log(`endConnection 异常: ${String(e)}`); | ||
| 108 | + } | ||
| 109 | + }; | ||
| 110 | + | ||
| 111 | + // 按类型获取商品信息:使用「商品类型」+「商品 ID 列表」,对应 IAP Kit queryProducts | ||
| 112 | + getItemsByType = async () => { | ||
| 113 | + const productIds = this.state.skuIds | ||
| 114 | + .split(',') | ||
| 115 | + .map(s => s.trim()) | ||
| 116 | + .filter(Boolean); | ||
| 117 | + this.setState({ getItemsByTypeLoading: true }); | ||
| 118 | + try { | ||
| 119 | + const type = this.state.productType; | ||
| 120 | + const params = { skus: [productIds] }; | ||
| 121 | + let items = | ||
| 122 | + type === 'subs' || type === 'non_renewable' | ||
| 123 | + ? await getSubscriptions(params) | ||
| 124 | + : await getProducts(params); | ||
| 125 | + const list = Array.isArray(items) | ||
| 126 | + ? (items as Record<string, unknown>[]) | ||
| 127 | + : []; | ||
| 128 | + this.setState({ productList: list, getItemsByTypeLoading: false }); | ||
| 129 | + const successMsg = `按类型获取商品(类型=${type}, ID列表=${productIds.join( | ||
| 130 | + ', ', | ||
| 131 | + )}): ${list.length} 个`; | ||
| 132 | + console.log(successMsg); | ||
| 133 | + if (list.length > 0) { | ||
| 134 | + list.forEach((item, i) => { | ||
| 135 | + console.log('商品信息: '+JSON.stringify(item)); | ||
| 136 | + }); | ||
| 137 | + } | ||
| 138 | + Alert.alert( | ||
| 139 | + '查询成功', | ||
| 140 | + `已获取 ${list.length} 个商品信息,可在下方列表中查看并购买。`, | ||
| 141 | + ); | ||
| 142 | + } catch (e) { | ||
| 143 | + this.setState({ productList: [], getItemsByTypeLoading: false }); | ||
| 144 | + const errMsg = `按类型获取商品 异常: ${JSON.stringify(e)}`; | ||
| 145 | + console.log(errMsg); | ||
| 146 | + Alert.alert('查询失败', '获取商品列表失败,请检查网络或商品 ID 后重试。'); | ||
| 147 | + } | ||
| 148 | + }; | ||
| 149 | + | ||
| 150 | + //查询未完成订单(对应 getAvailablePurchases,用于完成交易前选择一条记录) | ||
| 151 | + fetchAvailablePurchases = async () => { | ||
| 152 | + try { | ||
| 153 | + const list = (await getAvailablePurchases?.({})) ?? []; | ||
| 154 | + const arr = Array.isArray(list) | ||
| 155 | + ? (list as Record<string, unknown>[]) | ||
| 156 | + : []; | ||
| 157 | + this.setState({ | ||
| 158 | + availablePurchases: arr, | ||
| 159 | + availablePurchasesLoading: false, | ||
| 160 | + }); | ||
| 161 | + console.log(`getAvailablePurchases: ${arr.length} 条`); | ||
| 162 | + console.log( | ||
| 163 | + `getAvailablePurchases list 是: ${JSON.stringify( | ||
| 164 | + list, | ||
| 165 | + )},${typeof list},${typeof arr} 条`, | ||
| 166 | + ); | ||
| 167 | + if (JSON.stringify(list) == '{}') { | ||
| 168 | + Alert.alert('查询结果', '尚未初始化。'); | ||
| 169 | + } else if (arr.length <= 0) { | ||
| 170 | + Alert.alert('查询结果', '当前没有未完成的订单。'); | ||
| 171 | + } | ||
| 172 | + } catch (e) { | ||
| 173 | + this.setState({ | ||
| 174 | + availablePurchases: [], | ||
| 175 | + availablePurchasesLoading: false, | ||
| 176 | + }); | ||
| 177 | + console.log(`getAvailablePurchases 异常: ${String(e)}`); | ||
| 178 | + Alert.alert('查询失败', JSON.stringify(e)); | ||
| 179 | + } | ||
| 180 | + }; | ||
| 181 | + | ||
| 182 | + // 购买商品:使用「商品类型」+「商品 ID 列表」(取第一个为 productId),对应 IAP Kit createPurchase | ||
| 183 | + buyItemByType = async () => { | ||
| 184 | + const skus = this.state.skuIds | ||
| 185 | + .split(',') | ||
| 186 | + .map(s => s.trim()) | ||
| 187 | + .filter(Boolean); | ||
| 188 | + if (skus.length === 0) { | ||
| 189 | + Alert.alert('提示', '请至少填写一个商品 ID'); | ||
| 190 | + return; | ||
| 191 | + } | ||
| 192 | + const productId = skus[0]; | ||
| 193 | + const type = this.state.productType; | ||
| 194 | + const isSubscription = type === 'subs' || type === 'non_renewable'; | ||
| 195 | + const request = { | ||
| 196 | + sku: productId, | ||
| 197 | + appAccountToken:'xxxxx-xxx-xxxxx', | ||
| 198 | + quantity: 10, | ||
| 199 | + promotionalOfferId: '' | ||
| 200 | + }; | ||
| 201 | + try { | ||
| 202 | + const result = isSubscription | ||
| 203 | + ? await requestSubscription(request) | ||
| 204 | + : await requestPurchase(request); | ||
| 205 | + const resultStr = JSON.stringify(result); | ||
| 206 | + console.log( | ||
| 207 | + `${ | ||
| 208 | + isSubscription ? 'requestSubscription' : 'requestPurchase' | ||
| 209 | + } 结果: ${resultStr}`, | ||
| 210 | + ); | ||
| 211 | + const token = | ||
| 212 | + (result as { purchaseToken?: string })?.purchaseToken ?? | ||
| 213 | + (result as { orderId?: string })?.orderId; | ||
| 214 | + if (result) { | ||
| 215 | + this.setState({ purchaseToken: String(token) }); | ||
| 216 | + console.log('购买成功,点击「确认购买」确认收货'); | ||
| 217 | + Alert.alert('支付成功', '支付已完成,点击「确认购买」确认收货。'); | ||
| 218 | + } else { | ||
| 219 | + Alert.alert('购买完成', '支付未完成,请稍后重试或检查网络与支付方式。'); | ||
| 220 | + } | ||
| 221 | + } catch (e) { | ||
| 222 | + const errMsg = `购买异常: ${JSON.stringify(e)}`; | ||
| 223 | + console.log(errMsg); | ||
| 224 | + Alert.alert('支付失败', '支付未完成,请稍后重试或检查网络与支付方式。'); | ||
| 225 | + } | ||
| 226 | + }; | ||
| 227 | + | ||
| 228 | + // 确认购买(对应 IAP Kit:finishPurchase,非消耗型/订阅;购买成功时已自动调用,此处用于手动重试) | ||
| 229 | + acknowledgePurchase = async () => { | ||
| 230 | + try { | ||
| 231 | + for (let i = 0; i < this.state.availablePurchases.length; i++) | ||
| 232 | + { | ||
| 233 | + const purchase = this.state.availablePurchases[i] | ||
| 234 | + console.debug('iap => '+JSON.stringify(purchase.productTypeIos)) | ||
| 235 | + const isConsumable = purchase.productTypeIos === 'inapp'; | ||
| 236 | + const result = await finishTransaction({ purchase, isConsumable }); | ||
| 237 | + console.debug('iap => '+JSON.stringify(result)) | ||
| 238 | + if (result) { | ||
| 239 | + Alert.alert('确认成功', '订单已确认,商品权益已生效。'); | ||
【AI-Review】【一般】【基础代码问题】【代码逻辑错误】acknowledgePurchase 循环中调用 Alert.alert 导致多次提示被覆盖 ● 问题: 触发路径:用户点击"确认完成所有订单"按钮 → ● 影响:一般。当有多条未完成订单且部分成功、部分失败时,用户无法从 UI 上得知每条订单的处理结果,只看到最后一个订单的提示,影响测试用例对 ● 建议:将循环中的
![]() ![]() | |||
| 240 | + } else { | ||
| 241 | + Alert.alert('确认失败', '订单确认失败,请稍后重试。'); | ||
| 242 | + } | ||
| 243 | + } | ||
| 244 | + } catch (e) { | ||
| 245 | + const errMsg = `finishTransaction 异常: ${String(e)}`; | ||
| 246 | + console.log(errMsg); | ||
| 247 | + Alert.alert('确认失败', '订单确认失败,请稍后重试。'); | ||
| 248 | + } | ||
| 249 | + }; | ||
| 250 | + | ||
| 251 | + // 开始监听购买更新(鸿蒙端可在此注册购买结果回调) | ||
| 252 | + UpdatedListener = () => { | ||
| 253 | + try { | ||
| 254 | + this.purchaseUpdatedSubscription = purchaseUpdatedListener((purchase:Purchase)=>{ | ||
| 255 | + console.log('购买成功事件:', purchase); | ||
| 256 | + // 使用格式化 JSON,确保所有字段都被记录 | ||
| 257 | + const purchaseStr = JSON.stringify(purchase, null, 2); | ||
| 258 | + this.appendLog(`购买成功:\n${purchaseStr}`); | ||
| 259 | + }); | ||
| 260 | + | ||
| 261 | + this.setState({ UpdatedlisteningStatus: 'ok' }); | ||
| 262 | + console.log('purchaseUpdatedListener 已调用,事件监听器已注册'); | ||
| 263 | + } catch (e) { | ||
| 264 | + this.setState({ UpdatedlisteningStatus: 'fail' }); | ||
| 265 | + console.log(`purchaseUpdatedListener 异常: ${String(e)}`); | ||
| 266 | + } | ||
| 267 | + }; | ||
| 268 | + | ||
| 269 | + // 开始监听购买更新(鸿蒙端可在此注册购买结果回调) | ||
| 270 | + ErrorListener = () => { | ||
| 271 | + try { | ||
| 272 | + | ||
| 273 | + this.purchaseErrorSubscription = purchaseErrorListener((error:PurchaseError)=>{ | ||
| 274 | + console.log('购买失败事件:', error); | ||
| 275 | + // 使用格式化 JSON,确保所有字段都被记录 | ||
| 276 | + const errorStr = JSON.stringify(error, null, 2); | ||
| 277 | + this.appendLog(`购买失败:\n${errorStr}`); | ||
| 278 | + }); | ||
| 279 | + | ||
| 280 | + this.setState({ ErrorlisteningStatus: 'ok' }); | ||
| 281 | + console.log('purchaseErrorListener 已调用,事件监听器已注册'); | ||
| 282 | + } catch (e) { | ||
| 283 | + this.setState({ ErrorlisteningStatus: 'fail' }); | ||
| 284 | + console.log(`purchaseErrorListener 异常: ${String(e)}`); | ||
| 285 | + } | ||
| 286 | + }; | ||
| 287 | + | ||
| 288 | + // 停止监听购买更新 | ||
| 289 | + stopUpdatedListener = () => { | ||
| 290 | + try { | ||
| 291 | + if (this.purchaseUpdatedSubscription) { | ||
| 292 | + this.purchaseUpdatedSubscription.remove(); | ||
| 293 | + this.purchaseUpdatedSubscription = null; | ||
| 294 | + } | ||
| 295 | + | ||
| 296 | + this.setState({ UpdatedlisteningStatus: 'stopped' }); | ||
| 297 | + console.log('stopListening 已调用,事件监听器已移除'); | ||
| 298 | + } catch (e) { | ||
| 299 | + console.log(`stopListening 异常: ${String(e)}`); | ||
| 300 | + } | ||
| 301 | + }; | ||
| 302 | + | ||
| 303 | + // 停止监听购买更新 | ||
| 304 | + stopErrorListener = () => { | ||
| 305 | + try { | ||
| 306 | + if (this.purchaseErrorSubscription) { | ||
| 307 | + this.purchaseErrorSubscription.remove(); | ||
| 308 | + this.purchaseErrorSubscription = null; | ||
| 309 | + } | ||
| 310 | + this.setState({ ErrorlisteningStatus: 'stopped' }); | ||
| 311 | + console.log('stopErrorListener 已调用,事件监听器已移除'); | ||
| 312 | + } catch (e) { | ||
| 313 | + console.log(`stopErrorListener 异常: ${String(e)}`); | ||
| 314 | + } | ||
| 315 | + }; | ||
| 316 | + | ||
| 317 | + clearLog = () => this.setState({ log: '' }); | ||
| 318 | + | ||
| 319 | + // 获取购买历史记录 | ||
| 320 | + fetchPurchaseHistory = async () => { | ||
| 321 | + try { | ||
| 322 | + const list = await getPurchaseHistory({ | ||
| 323 | + alsoPublishToEventListener: false, | ||
| 324 | + automaticallyFinishRestoredTransactions: false, | ||
| 325 | + onlyIncludeActiveItems: false, | ||
| 326 | + }); | ||
| 327 | + const arr = Array.isArray(list) | ||
| 328 | + ? (list as Record<string, unknown>[]) | ||
| 329 | + : []; | ||
| 330 | + this.setState({ purchaseHistory: arr }); | ||
| 331 | + console.log(`getPurchaseHistory: ${arr.length} 条`); | ||
| 332 | + Alert.alert('查询成功', `共 ${arr.length} 条购买记录`); | ||
| 333 | + } catch (e) { | ||
| 334 | + this.setState({ purchaseHistory: [] }); | ||
| 335 | + console.log(`getPurchaseHistory 异常: ${String(e)}`); | ||
| 336 | + Alert.alert('查询失败', JSON.stringify(e)); | ||
| 337 | + } | ||
| 338 | + }; | ||
| 339 | + | ||
| 340 | + // 获取应用交易信息 | ||
| 341 | + fetchAppTransaction = async () => { | ||
| 342 | + try { | ||
| 343 | + const result = await getAppTransaction(); | ||
| 344 | + this.setState({ appTransaction: result }); | ||
| 345 | + console.log(`getAppTransaction: ${JSON.stringify(result)}`); | ||
| 346 | + if (result) { | ||
| 347 | + Alert.alert('查询成功', `应用交易信息: ${JSON.stringify(result, null, 2)}`); | ||
| 348 | + } else { | ||
| 349 | + Alert.alert('查询结果', '未找到应用交易信息'); | ||
| 350 | + } | ||
| 351 | + } catch (e) { | ||
| 352 | + this.setState({ appTransaction: null }); | ||
| 353 | + console.log(`getAppTransaction 异常: ${String(e)}`); | ||
| 354 | + Alert.alert('查询失败', JSON.stringify(e)); | ||
| 355 | + } | ||
| 356 | + }; | ||
| 357 | + | ||
| 358 | + // 跳转订阅管理页面 | ||
| 359 | + openSubscriptionManage = async () => { | ||
| 360 | + const sku = this.state.subscriptionSku; | ||
| 361 | + try { | ||
| 362 | + console.log(`deepLinkToSubscriptions: ${sku}`); | ||
| 363 | + await deepLinkToSubscriptions({sku}); | ||
| 364 | + } catch (e) { | ||
| 365 | + console.log(`deepLinkToSubscriptions 异常: ${String(e)}`); | ||
| 366 | + Alert.alert('跳转失败', JSON.stringify(e)); | ||
| 367 | + } | ||
| 368 | + }; | ||
| 369 | + | ||
| 370 | + componentWillUnmount() { | ||
| 371 | + if (this.purchaseUpdatedSubscription) { | ||
| 372 | + this.purchaseUpdatedSubscription.remove(); | ||
| 373 | + this.purchaseUpdatedSubscription = null; | ||
| 374 | + } | ||
| 375 | + if (this.purchaseErrorSubscription) { | ||
| 376 | + this.purchaseErrorSubscription.remove(); | ||
| 377 | + this.purchaseErrorSubscription = null; | ||
| 378 | + } | ||
| 379 | + } | ||
| 380 | + | ||
| 381 | + render() { | ||
| 382 | + if (this.state.showTestPage) { | ||
| 383 | + return ( | ||
| 384 | + <View style={styles.container}> | ||
| 385 | + <View style={styles.row}> | ||
| 386 | + <Button title="返回" onPress={() => this.setState({ showTestPage: false })} /> | ||
| 387 | + </View> | ||
| 388 | + <TestPage /> | ||
| 389 | + </View> | ||
| 390 | + ); | ||
| 391 | + } | ||
| 392 | + return ( | ||
| 393 | + <ScrollView> | ||
| 394 | + <Tester> | ||
| 395 | + <TestSuite name="IAP 示例"> | ||
| 396 | + <Text style={styles.status}> | ||
| 397 | + 连接状态: {this.state.connected ? '已连接' : '未连接'} | ||
| 398 | + </Text> | ||
| 399 | + | ||
| 400 | + <View> | ||
| 401 | + <View style={{ marginVertical: 6 }}> | ||
| 402 | + <TestCase itShould="initConnection"> | ||
| 403 | + <Button title="初始化连接" onPress={this.initConnection} /> | ||
| 404 | + </TestCase> | ||
| 405 | + </View> | ||
| 406 | + <View style={{ marginVertical: 6 }}> | ||
| 407 | + <TestCase itShould="endConnection"> | ||
| 408 | + <Button title="结束初始化连接" onPress={this.endConnection} /> | ||
| 409 | + </TestCase> | ||
| 410 | + </View> | ||
| 411 | + </View> | ||
| 412 | + <Text style={styles.productTypeHint}> | ||
| 413 | + inapp:消耗型 | subs:自动续期订阅 | ||
| 414 | + </Text> | ||
| 415 | + <Text style={styles.label}>获取商品</Text> | ||
| 416 | + <CustomDropdown | ||
| 417 | + data={this.state.list} | ||
| 418 | + onSelect={item => { | ||
| 419 | + if (item == null) return; | ||
| 420 | + if (typeof item === 'string' || typeof item === 'number') { | ||
| 421 | + const typeStr = String(item); | ||
| 422 | + const found = this.state.list.find( | ||
| 423 | + i => String(i.type) === typeStr, | ||
| 424 | + ); | ||
| 425 | + this.setState({ | ||
| 426 | + productType: typeStr, | ||
| 427 | + skuIds: found ? String(found.id) : '', | ||
| 428 | + }); | ||
| 429 | + } else if (typeof item === 'object') { | ||
| 430 | + const typeStr = String((item as any).type ?? ''); | ||
| 431 | + const idStr = String((item as any).id ?? ''); | ||
| 432 | + this.setState({ productType: typeStr, skuIds: idStr }); | ||
| 433 | + } | ||
| 434 | + }} | ||
| 435 | + placeholder="请选择商品" | ||
| 436 | + /> | ||
| 437 | + <View style={{ marginVertical: 6 }}> | ||
| 438 | + <TestCase itShould="getProducts & getSubscriptions"> | ||
| 439 | + <Button | ||
| 440 | + title="按类型获取商品" | ||
| 441 | + onPress={this.getItemsByType} | ||
| 442 | + disabled={this.state.getItemsByTypeLoading} | ||
| 443 | + /> | ||
| 444 | + </TestCase> | ||
| 445 | + </View> | ||
| 446 | + {this.state.productList.length > 0 ? ( | ||
| 447 | + <View style={styles.productList}> | ||
| 448 | + <Text style={styles.productListTitle}> | ||
| 449 | + 当前商品列表 | ||
| 450 | + </Text> | ||
| 451 | + {this.state.productList.map((item, i) => ( | ||
| 452 | + <View key={i} style={styles.productItem}> | ||
| 453 | + <Text style={styles.productId}> | ||
| 454 | + {String( | ||
| 455 | + item.title ?? item.productName ?? item.productId ?? '-', | ||
| 456 | + )} | ||
| 457 | + </Text> | ||
| 458 | + <Text style={styles.productMeta}> | ||
| 459 | + ID: {String(item.productId ?? '-')} | ||
| 460 | + </Text> | ||
| 461 | + <View style={styles.productItemMetaRow}> | ||
| 462 | + <Text style={styles.productItemPrice}> | ||
| 463 | + 价格: {String(item.price ?? '-')} | ||
| 464 | + {item.currency ? ` ${item.currency}` : ''} | ||
| 465 | + </Text> | ||
| 466 | + {item.productType != null || item.type != null ? ( | ||
| 467 | + <Text style={styles.productItemTypeBadge}> | ||
| 468 | + {String( | ||
| 469 | + item.productType ?? | ||
| 470 | + item.type ?? | ||
| 471 | + this.state.productType, | ||
| 472 | + )} | ||
| 473 | + </Text> | ||
| 474 | + ) : null} | ||
| 475 | + </View> | ||
| 476 | + {item.description != null && | ||
| 477 | + String(item.description).length > 0 ? ( | ||
| 478 | + <Text style={styles.productDesc} numberOfLines={2}> | ||
| 479 | + {String(item.description)} | ||
| 480 | + </Text> | ||
| 481 | + ) : null} | ||
| 482 | + <View style={{ marginVertical: 6}}> | ||
| 483 | + <TestCase itShould="requestPurchase & requestSubscription"> | ||
| 484 | + <Button | ||
| 485 | + title="购买商品" | ||
| 486 | + onPress={() => | ||
| 487 | + this.selectProductForPurchase( | ||
| 488 | + String(item.productId ?? ''), | ||
| 489 | + String( | ||
| 490 | + item.productType ?? | ||
| 491 | + item.type ?? | ||
| 492 | + this.state.productType, | ||
| 493 | + ), | ||
| 494 | + ) | ||
| 495 | + } | ||
| 496 | + /> | ||
| 497 | + </TestCase> | ||
| 498 | + </View> | ||
| 499 | + </View> | ||
| 500 | + ))} | ||
| 501 | + </View> | ||
| 502 | + ) : ( | ||
| 503 | + <View style={styles.productListEmpty}> | ||
| 504 | + <Text style={styles.productListEmptyText}>暂无商品数据</Text> | ||
| 505 | + </View> | ||
| 506 | + )} | ||
| 507 | + | ||
| 508 | + <Text style={styles.section}>购买 / 确认 / 消耗</Text> | ||
| 509 | + <View style={{ marginVertical: 6 }}> | ||
| 510 | + <TestCase itShould="finishTransaction"> | ||
| 511 | + <Button | ||
| 512 | + title="确认完成所有订单" | ||
| 513 | + onPress={this.acknowledgePurchase} | ||
| 514 | + /> | ||
| 515 | + </TestCase> | ||
| 516 | + </View> | ||
| 517 | + | ||
| 518 | + <Text style={styles.section}>查询未完成订单</Text> | ||
| 519 | + <View style={{ marginVertical: 6 }}> | ||
| 520 | + <TestCase itShould="getAvailablePurchases"> | ||
| 521 | + <Button | ||
| 522 | + title="查询已购买但未消耗订单" | ||
| 523 | + onPress={this.fetchAvailablePurchases} | ||
| 524 | + disabled={this.state.availablePurchasesLoading} | ||
| 525 | + /> | ||
| 526 | + </TestCase> | ||
| 527 | + </View> | ||
| 528 | + {this.state.availablePurchasesLoading && ( | ||
| 529 | + <Text style={styles.resultText}>查询中...</Text> | ||
| 530 | + )} | ||
| 531 | + {!this.state.availablePurchasesLoading && | ||
| 532 | + this.state.availablePurchases.length > 0 && ( | ||
| 533 | + <Text style={[styles.resultText, styles.resultSuccess]}> | ||
| 534 | + 共 {this.state.availablePurchases.length} 条未完成订单 | ||
| 535 | + </Text> | ||
| 536 | + )} | ||
| 537 | + <View style={{ marginVertical: 6 }}> | ||
| 538 | + <TestCase itShould="getPurchaseHistory"> | ||
| 539 | + <Button | ||
| 540 | + title="查询购买历史" | ||
| 541 | + onPress={this.fetchPurchaseHistory} | ||
| 542 | + /> | ||
| 543 | + </TestCase> | ||
| 544 | + </View> | ||
| 545 | + {this.state.purchaseHistory.length > 0 && ( | ||
| 546 | + <Text style={[styles.resultText, styles.resultSuccess]}> | ||
| 547 | + 共 {this.state.purchaseHistory.length} 条购买记录 | ||
| 548 | + </Text> | ||
| 549 | + )} | ||
| 550 | + | ||
| 551 | + <View style={{ marginVertical: 6 }}> | ||
| 552 | + <TestCase itShould="getAppTransaction"> | ||
| 553 | + <Button | ||
| 554 | + title="获取应用交易信息" | ||
| 555 | + onPress={this.fetchAppTransaction} | ||
| 556 | + /> | ||
| 557 | + </TestCase> | ||
| 558 | + </View> | ||
| 559 | + <View style={{ marginVertical: 6 }}> | ||
| 560 | + <TestCase itShould="deepLinkToSubscriptions"> | ||
| 561 | + <Button | ||
| 562 | + title="打开订阅管理页面" | ||
| 563 | + onPress={this.openSubscriptionManage} | ||
| 564 | + /> | ||
| 565 | + </TestCase> | ||
| 566 | + </View> | ||
| 567 | + <View style={{ marginVertical: 6 }}> | ||
| 568 | + <TestCase itShould="purchaseUpdatedListener"> | ||
| 569 | + <Button | ||
| 570 | + title={ | ||
| 571 | + this.state.UpdatedlisteningStatus === 'ok' | ||
| 572 | + ? '停止监听购买更新事件' | ||
| 573 | + : '开始监听购买更新事件' | ||
| 574 | + } | ||
| 575 | + onPress={ | ||
| 576 | + this.state.UpdatedlisteningStatus === 'ok' | ||
| 577 | + ? this.stopUpdatedListener | ||
| 578 | + : this.UpdatedListener | ||
| 579 | + } | ||
| 580 | + /> | ||
| 581 | + </TestCase> | ||
| 582 | + </View> | ||
| 583 | + <View style={{ marginTop: 6, marginBottom: 4 }}> | ||
| 584 | + {this.state.UpdatedlisteningStatus !== null ? ( | ||
| 585 | + <Text | ||
| 586 | + style={[ | ||
| 587 | + styles.resultText, | ||
| 588 | + this.state.UpdatedlisteningStatus === 'ok' | ||
| 589 | + ? styles.resultSuccess | ||
| 590 | + : this.state.UpdatedlisteningStatus === 'stopped' | ||
| 591 | + ? styles.resultText | ||
| 592 | + : styles.resultFail, | ||
| 593 | + ]}> | ||
| 594 | + 监听:{' '} | ||
| 595 | + {this.state.UpdatedlisteningStatus === 'ok' | ||
| 596 | + ? '已开始' | ||
| 597 | + : this.state.UpdatedlisteningStatus === 'stopped' | ||
| 598 | + ? '已停止' | ||
| 599 | + : '失败'} | ||
| 600 | + </Text> | ||
| 601 | + ) : null} | ||
| 602 | + </View> | ||
| 603 | + | ||
| 604 | + | ||
| 605 | + <View style={{ marginVertical: 6 }}> | ||
| 606 | + <TestCase itShould="purchaseErrorListener"> | ||
| 607 | + <Button | ||
| 608 | + title={ | ||
| 609 | + this.state.ErrorlisteningStatus === 'ok' | ||
| 610 | + ? '停止监听购买错误事件' | ||
| 611 | + : '开始监听购买错误事件' | ||
| 612 | + } | ||
| 613 | + onPress={ | ||
| 614 | + this.state.ErrorlisteningStatus === 'ok' | ||
| 615 | + ? this.stopErrorListener | ||
| 616 | + : this.ErrorListener | ||
| 617 | + } | ||
| 618 | + /> | ||
| 619 | + </TestCase> | ||
| 620 | + </View> | ||
| 621 | + <View style={{ marginTop: 6, marginBottom: 4 }}> | ||
| 622 | + {this.state.ErrorlisteningStatus !== null ? ( | ||
| 623 | + <Text | ||
| 624 | + style={[ | ||
| 625 | + styles.resultText, | ||
| 626 | + this.state.ErrorlisteningStatus === 'ok' | ||
| 627 | + ? styles.resultSuccess | ||
| 628 | + : this.state.ErrorlisteningStatus === 'stopped' | ||
| 629 | + ? styles.resultText | ||
| 630 | + : styles.resultFail, | ||
| 631 | + ]}> | ||
| 632 | + 监听:{' '} | ||
| 633 | + {this.state.ErrorlisteningStatus === 'ok' | ||
| 634 | + ? '已开始' | ||
| 635 | + : this.state.ErrorlisteningStatus === 'stopped' | ||
| 636 | + ? '已停止' | ||
| 637 | + : '失败'} | ||
| 638 | + </Text> | ||
| 639 | + ) : null} | ||
| 640 | + </View> | ||
| 641 | + | ||
| 642 | + <View style={{ marginVertical: 6 }}> | ||
| 643 | + <TestCase itShould="clearLog"> | ||
| 644 | + <Button title="清空监听日志" onPress={this.clearLog} /> | ||
| 645 | + </TestCase> | ||
| 646 | + </View> | ||
| 647 | + <View style={{ marginVertical: 6 }}> | ||
| 648 | + {/* <TestCase itShould="useIAP"> */} | ||
| 649 | + <Button title="自定义Hook" onPress={ | ||
| 650 | + ()=>{ | ||
| 651 | + this.setState({ showTestPage: true }) | ||
| 652 | + } | ||
| 653 | + } /> | ||
| 654 | + </View> | ||
| 655 | + <Text style={styles.logLabel}>监听日志</Text> | ||
| 656 | + <Text style={styles.log} selectable> | ||
| 657 | + {this.state.log || '(无)'} | ||
| 658 | + </Text> | ||
| 659 | + </TestSuite> | ||
| 660 | + </Tester> | ||
| 661 | + </ScrollView> | ||
| 662 | + ); | ||
| 663 | + } | ||
| 664 | +} | ||
| 665 | + | ||
| 666 | +const styles = StyleSheet.create({ | ||
| 667 | + container: { | ||
| 668 | + flex: 1, | ||
| 669 | + backgroundColor: '#f3f6f9', | ||
| 670 | + }, | ||
| 671 | + content: { | ||
| 672 | + padding: 16, | ||
| 673 | + paddingBottom: 32, | ||
| 674 | + backgroundColor: '#ffffff', | ||
| 675 | + borderRadius: 8, | ||
| 676 | + margin: 12, | ||
| 677 | + }, | ||
| 678 | + label: { | ||
| 679 | + fontSize: 24, | ||
| 680 | + marginBottom: 8, | ||
| 681 | + color: '#fcfbfbff', | ||
| 682 | + }, | ||
| 683 | + status: { | ||
| 684 | + fontSize: 14, | ||
| 685 | + marginVertical: 4, | ||
| 686 | + color: '#f8f8f8ff', | ||
| 687 | + }, | ||
| 688 | + flowHint: { | ||
| 689 | + fontSize: 12, | ||
| 690 | + marginVertical: 6, | ||
| 691 | + color: '#faf9f9ff', | ||
| 692 | + fontStyle: 'italic', | ||
| 693 | + }, | ||
| 694 | + section: { | ||
| 695 | + fontSize: 16, | ||
| 696 | + fontWeight: '600', | ||
| 697 | + marginTop: 16, | ||
| 698 | + marginBottom: 8, | ||
| 699 | + color: '#FFFFFF', | ||
| 700 | + backgroundColor: '#333', | ||
| 701 | + paddingVertical: 6, | ||
| 702 | + paddingHorizontal: 10, | ||
| 703 | + borderRadius: 6, | ||
| 704 | + alignSelf: 'flex-start', | ||
| 705 | + }, | ||
| 706 | + row: { | ||
| 707 | + flexDirection: 'row', | ||
| 708 | + flexWrap: 'wrap', | ||
| 709 | + gap: 8, | ||
| 710 | + marginVertical: 6, | ||
| 711 | + width: '100%', | ||
| 712 | + marginLeft: 15, | ||
| 713 | + marginTop: 38, | ||
| 714 | + }, | ||
| 715 | + input: { | ||
| 716 | + borderWidth: 1, | ||
| 717 | + borderColor: '#ccc', | ||
| 718 | + backgroundColor: '#fff', | ||
| 719 | + color: '#0a0a0a', | ||
| 720 | + borderRadius: 6, | ||
| 721 | + padding: 10, | ||
| 722 | + marginVertical: 8, | ||
| 723 | + fontSize: 14, | ||
| 724 | + }, | ||
| 725 | + logLabel: { | ||
| 726 | + fontSize: 16, | ||
| 727 | + color: '#FFF', | ||
| 728 | + fontWeight: '600', | ||
| 729 | + marginTop: 16, | ||
| 730 | + marginBottom: 6, | ||
| 731 | + }, | ||
| 732 | + log: { | ||
| 733 | + fontSize: 12, | ||
| 734 | + color: '#555', | ||
| 735 | + backgroundColor: '#f5f5f5', | ||
| 736 | + padding: 10, | ||
| 737 | + borderRadius: 6, | ||
| 738 | + minHeight: 80, | ||
| 739 | + }, | ||
| 740 | + hint: { | ||
| 741 | + fontSize: 11, | ||
| 742 | + color: '#FFFFFF', | ||
| 743 | + marginTop: 4, | ||
| 744 | + marginBottom: 8, | ||
| 745 | + }, | ||
| 746 | + resultText: { | ||
| 747 | + fontSize: 14, | ||
| 748 | + marginTop: 6, | ||
| 749 | + marginBottom: 4, | ||
| 750 | + color: '#666', | ||
| 751 | + }, | ||
| 752 | + resultSuccess: { | ||
| 753 | + color: '#2e7d32', | ||
| 754 | + fontWeight: '600', | ||
| 755 | + }, | ||
| 756 | + resultFail: { | ||
| 757 | + color: '#c62828', | ||
| 758 | + fontWeight: '600', | ||
| 759 | + }, | ||
| 760 | + productTypeHint: { | ||
| 761 | + fontSize: 12, | ||
| 762 | + color: '#faf9f9ff', | ||
| 763 | + marginBottom: 8, | ||
| 764 | + lineHeight: 18, | ||
| 765 | + }, | ||
| 766 | + productList: { | ||
| 767 | + marginTop: 8, | ||
| 768 | + marginBottom: 8, | ||
| 769 | + }, | ||
| 770 | + productListTitle: { | ||
| 771 | + fontSize: 16, | ||
| 772 | + fontWeight: '600', | ||
| 773 | + color: '#ffffff', | ||
| 774 | + marginTop: 0, | ||
| 775 | + marginBottom: 8, | ||
| 776 | + }, | ||
| 777 | + productItem: { | ||
| 778 | + padding: 12, | ||
| 779 | + marginVertical: 6, | ||
| 780 | + backgroundColor: '#ffffff', | ||
| 781 | + borderRadius: 6, | ||
| 782 | + borderWidth: 1, | ||
| 783 | + borderColor: '#eee', | ||
| 784 | + shadowColor: '#000', | ||
| 785 | + shadowOpacity: 0.03, | ||
| 786 | + shadowRadius: 4, | ||
| 787 | + elevation: 1, | ||
| 788 | + }, | ||
| 789 | + productId: { | ||
| 790 | + fontSize: 14, | ||
| 791 | + fontWeight: '600', | ||
| 792 | + color: '#111111', | ||
| 793 | + }, | ||
| 794 | + productMeta: { | ||
| 795 | + fontSize: 12, | ||
| 796 | + color: '#555555', | ||
| 797 | + marginTop: 4, | ||
| 798 | + }, | ||
| 799 | + productItemMetaRow: { | ||
| 800 | + flexDirection: 'row', | ||
| 801 | + alignItems: 'center', | ||
| 802 | + gap: 10, | ||
| 803 | + marginTop: 6, | ||
| 804 | + }, | ||
| 805 | + productItemPrice: { | ||
| 806 | + fontSize: 14, | ||
| 807 | + fontWeight: '600', | ||
| 808 | + color: '#c62828', | ||
| 809 | + }, | ||
| 810 | + productItemTypeBadge: { | ||
| 811 | + fontSize: 11, | ||
| 812 | + color: '#666', | ||
| 813 | + paddingHorizontal: 6, | ||
| 814 | + paddingVertical: 2, | ||
| 815 | + backgroundColor: '#eee', | ||
| 816 | + borderRadius: 4, | ||
| 817 | + }, | ||
| 818 | + productDesc: { | ||
| 819 | + fontSize: 11, | ||
| 820 | + color: '#666666', | ||
| 821 | + marginTop: 6, | ||
| 822 | + fontStyle: 'italic', | ||
| 823 | + }, | ||
| 824 | + productItemActions: { | ||
| 825 | + marginTop: 8, | ||
| 826 | + }, | ||
| 827 | + productListEmpty: { | ||
| 828 | + padding: 16, | ||
| 829 | + marginTop: 8, | ||
| 830 | + marginBottom: 8, | ||
| 831 | + backgroundColor: '#ffffff', | ||
| 832 | + borderRadius: 8, | ||
| 833 | + borderWidth: 1, | ||
| 834 | + borderColor: '#eee', | ||
| 835 | + }, | ||
| 836 | + productListEmptyText: { | ||
| 837 | + fontSize: 14, | ||
| 838 | + color: '#444444', | ||
| 839 | + marginBottom: 6, | ||
| 840 | + }, | ||
| 841 | + productListEmptyHint: { | ||
| 842 | + fontSize: 12, | ||
| 843 | + color: '#000000ff', | ||
| 844 | + lineHeight: 18, | ||
| 845 | + }, | ||
| 846 | +}); | ||
| 847 | + | ||
| 848 | +export const displayName = 'react-native-iap'; | ||
| 849 | +export const framework = 'React'; | ||
| 850 | +export const category = 'Base'; | ||
| 851 | +export const title = 'react-native-iap'; | ||
| 852 | +export const documentationURL = 'https://gitcode.com/CPF-RN/usage-docs/blob/br_rnoh0.61/zh-cn/react-native-iap.md'; | ||
| 853 | +export const description = 'react-native-iap'; | ||
| 854 | +export const examples = [ | ||
| 855 | + { | ||
| 856 | + title: 'react-native-iap', | ||
| 857 | + render: function (): any { | ||
| 858 | + return <AppDemo />; | ||
| 859 | + }, | ||
| 860 | + }, | ||
| 861 | + ]; | ||
| @@ -0,0 +1,247 @@ | |||
| 1 | +import React from 'react'; | ||
| 2 | +import { View, Text, Button, StyleSheet, Alert, ScrollView } from 'react-native'; | ||
| 3 | +import { requestPurchase, useIAP, withIAPContext } from 'react-native-iap'; | ||
| 4 | + | ||
| 5 | +const App: JSX.Element = () => { | ||
| 6 | + const { | ||
| 7 | + connected, | ||
| 8 | + products, | ||
| 9 | + subscriptions, | ||
| 10 | + purchaseHistory, | ||
| 11 | + availablePurchases, | ||
| 12 | + currentPurchase, | ||
| 13 | + currentPurchaseError, | ||
| 14 | + initConnectionError, | ||
| 15 | + finishTransaction, | ||
| 16 | + getProducts, | ||
| 17 | + getSubscriptions, | ||
| 18 | + getAvailablePurchases, | ||
| 19 | + getPurchaseHistory, | ||
| 20 | + clearCurrentPurchase, | ||
| 21 | + clearCurrentPurchaseError, | ||
| 22 | + restorePurchases, | ||
| 23 | + requestSubscription, | ||
| 24 | + } = useIAP({ | ||
| 25 | + onPurchaseSuccess: (purchase)=>{ | ||
| 26 | + Alert.alert('PurchaseSuccess',JSON.stringify(purchase)) | ||
| 27 | + }, | ||
| 28 | + onPurchaseError: (PurchaseError)=>{ | ||
| 29 | + Alert.alert('PurchaseError',JSON.stringify(PurchaseError)) | ||
| 30 | + }, | ||
| 31 | + }); | ||
| 32 | + | ||
| 33 | + const [textChoose, setTextChoose] = React.useState(-1) | ||
| 34 | + | ||
| 35 | + const handlePurchase = async (sku: string) => { | ||
| 36 | + await requestPurchase({ sku }); | ||
| 37 | + }; | ||
| 38 | + | ||
| 39 | + const handleSubscriptionPurchase = async (sku: string) => { | ||
| 40 | + await requestSubscription({ sku }); | ||
| 41 | + }; | ||
| 42 | + | ||
| 43 | + const getSub = async () => { | ||
| 44 | + await getSubscriptions({ skus: ['SubProduct01001'] }) | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + const getAvailable = async () => { | ||
| 48 | + await getAvailablePurchases() | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + const getHistory = async () => { | ||
| 52 | + await getPurchaseHistory() | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + const finish = async (): Promise<void> => { | ||
| 56 | + for (let i = 0; i < availablePurchases.length; i++) { | ||
| 57 | + const purchase = availablePurchases[i] | ||
| 58 | + console.debug('iap = > ' + JSON.stringify(purchase.productTypeIos)) | ||
| 59 | + const isConsumable = purchase.productTypeIos === 'inapp'; | ||
| 60 | + const result = await finishTransaction({ purchase, isConsumable }); | ||
| 61 | + console.debug('iap = > ' + JSON.stringify(result)) | ||
| 62 | + if (result) { | ||
| 63 | + Alert.alert('确认成功', '订单已确认,商品权益已生效。'); | ||
| 64 | + } else { | ||
| 65 | + Alert.alert('确认失败', '订单确认失败,请稍后重试。'); | ||
| 66 | + } | ||
| 67 | + } | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + return ( | ||
| 71 | + <ScrollView> | ||
| 72 | + <View style={styles.container}> | ||
| 73 | + <View style={styles.state}> | ||
| 74 | + <ScrollView> | ||
| 75 | + <Text style={styles.textSize}>{textChoose === 0 && JSON.stringify(subscriptions) !== '[]' ? JSON.stringify(subscriptions) : ''}</Text> | ||
| 76 | + <Text style={styles.textSize}>{textChoose === 1 && JSON.stringify(availablePurchases) !== '[]' ? JSON.stringify(availablePurchases) : ''}</Text> | ||
| 77 | + <Text style={styles.textSize}>{textChoose === 2 && JSON.stringify(purchaseHistory) !== '[]' ? JSON.stringify(purchaseHistory) : ''}</Text> | ||
| 78 | + <Text style={styles.textSize}>{textChoose === 3 ? JSON.stringify(currentPurchase) : ''}</Text> | ||
| 79 | + <Text style={styles.textSize}>{textChoose === 4 ? JSON.stringify(currentPurchaseError) : ''}</Text> | ||
| 80 | + <Text style={styles.textSize}>{textChoose === 5 ? JSON.stringify(initConnectionError) : ''}</Text> | ||
| 81 | + <Text style={styles.textSize}>{textChoose === 6 && JSON.stringify(products) !== '[]' ? JSON.stringify(products) : ''}</Text> | ||
| 82 | + </ScrollView> | ||
| 83 | + </View> | ||
| 84 | + | ||
| 85 | + <View style={styles.buttonBox}> | ||
| 86 | + <Button | ||
| 87 | + title="getSubscriptions" | ||
| 88 | + onPress={() => { | ||
| 89 | + getSub(); | ||
| 90 | + setTextChoose(0); | ||
| 91 | + }} | ||
| 92 | + /> | ||
| 93 | + </View> | ||
| 94 | + | ||
| 95 | + <View style={styles.buttonBox}> | ||
| 96 | + <Button | ||
| 97 | + title="getAvailablePurchases" | ||
| 98 | + onPress={() => { | ||
| 99 | + getAvailable(); | ||
| 100 | + setTextChoose(1); | ||
| 101 | + }} | ||
| 102 | + /> | ||
| 103 | + </View> | ||
| 104 | + | ||
| 105 | + <View style={styles.buttonBox}> | ||
| 106 | + <Button | ||
| 107 | + title="getPurchaseHistory" | ||
| 108 | + onPress={() => { | ||
| 109 | + getHistory(); | ||
| 110 | + setTextChoose(2); | ||
| 111 | + }} | ||
| 112 | + /> | ||
| 113 | + </View> | ||
| 114 | + | ||
| 115 | + <View style={styles.buttonBox}> | ||
| 116 | + <Button | ||
| 117 | + title="finishTransaction" | ||
| 118 | + onPress={() => { | ||
| 119 | + finish(); | ||
| 120 | + }} | ||
| 121 | + /> | ||
| 122 | + </View> | ||
| 123 | + | ||
| 124 | + <View style={styles.buttonBox}> | ||
| 125 | + <Button | ||
| 126 | + title="isConnected" | ||
| 127 | + onPress={() => { | ||
| 128 | + Alert.alert('connected', JSON.stringify(connected)) | ||
| 129 | + }} | ||
| 130 | + /> | ||
| 131 | + </View> | ||
| 132 | + | ||
| 133 | + <View style={styles.buttonBox}> | ||
| 134 | + <Button | ||
| 135 | + title="currentPurchase" | ||
| 136 | + onPress={() => { | ||
| 137 | + setTextChoose(3); | ||
| 138 | + }} | ||
| 139 | + /> | ||
| 140 | + </View> | ||
| 141 | + | ||
| 142 | + <View style={styles.buttonBox}> | ||
| 143 | + <Button | ||
| 144 | + title="currentPurchaseError" | ||
| 145 | + onPress={() => { | ||
| 146 | + setTextChoose(4); | ||
| 147 | + }} | ||
| 148 | + /> | ||
| 149 | + </View> | ||
| 150 | + | ||
| 151 | + <View style={styles.buttonBox}> | ||
| 152 | + <Button | ||
| 153 | + title="initConnectionError" | ||
| 154 | + onPress={() => { | ||
| 155 | + setTextChoose(5); | ||
| 156 | + }} | ||
| 157 | + /> | ||
| 158 | + </View> | ||
| 159 | + | ||
| 160 | + | ||
| 161 | + <View style={styles.buttonBox}> | ||
| 162 | + <Button | ||
| 163 | + title="clearCurrentPurchase" | ||
| 164 | + onPress={() => { clearCurrentPurchase() }} | ||
| 165 | + /> | ||
| 166 | + </View> | ||
| 167 | + | ||
| 168 | + <View style={styles.buttonBox}> | ||
| 169 | + <Button | ||
| 170 | + title="clearCurrentPurchaseError" | ||
| 171 | + onPress={() => { clearCurrentPurchaseError() }} | ||
| 172 | + /> | ||
| 173 | + </View> | ||
| 174 | + | ||
| 175 | + <View style={styles.buttonBox}> | ||
| 176 | + <Button | ||
| 177 | + title="restorePurchases" | ||
| 178 | + onPress={() => { | ||
| 179 | + restorePurchases() | ||
| 180 | + setTextChoose(1); | ||
| 181 | + }} | ||
| 182 | + /> | ||
| 183 | + </View> | ||
| 184 | + | ||
| 185 | + <View style={styles.buttonBox}> | ||
| 186 | + <Button | ||
| 187 | + title="getProducts" | ||
| 188 | + onPress={() => { | ||
| 189 | + getProducts({ skus: ['ohos_consume_002'] }); | ||
| 190 | + setTextChoose(6); | ||
| 191 | + }} | ||
| 192 | + /> | ||
| 193 | + </View> | ||
| 194 | + | ||
| 195 | + {products.map((product) => ( | ||
| 196 | + <View style={styles.product} key={product.productId}> | ||
| 197 | + <Button | ||
| 198 | + title="Buyproduct" | ||
| 199 | + onPress={() => handlePurchase(product.productId)} | ||
| 200 | + /> | ||
| 201 | + </View> | ||
| 202 | + ))} | ||
| 203 | + | ||
| 204 | + {subscriptions.map((subscription) => ( | ||
| 205 | + <View style={styles.product} key={subscription.productId}> | ||
| 206 | + <Button | ||
| 207 | + title="Buysubscription" | ||
| 208 | + onPress={() => handleSubscriptionPurchase(subscription.productId)} | ||
| 209 | + /> | ||
| 210 | + </View> | ||
| 211 | + ))} | ||
| 212 | + </View> | ||
| 213 | + </ScrollView> | ||
| 214 | + | ||
| 215 | + ); | ||
| 216 | +}; | ||
| 217 | + | ||
| 218 | +const styles = StyleSheet.create({ | ||
| 219 | + container: { | ||
| 220 | + width: "100%", | ||
| 221 | + height: "100%", | ||
| 222 | + flexDirection: 'column', | ||
| 223 | + padding: 15 | ||
| 224 | + }, | ||
| 225 | + buttonBox: { | ||
| 226 | + marginTop: 30 | ||
| 227 | + }, | ||
| 228 | + state: { | ||
| 229 | + width: '100%', | ||
| 230 | + height: 300, | ||
| 231 | + backgroundColor: '#FFF', | ||
| 232 | + marginTop: 38, | ||
| 233 | + padding: 15, | ||
| 234 | + borderWidth: 1, | ||
| 235 | + borderRadius: 10, | ||
| 236 | + flexShrink: 1, | ||
| 237 | + numberOfLines: 8, | ||
| 238 | + }, | ||
| 239 | + textSize: { | ||
| 240 | + fontSize: 20, | ||
| 241 | + }, | ||
| 242 | + product: { | ||
| 243 | + marginTop: 30 | ||
| 244 | + } | ||
| 245 | +}) | ||
| 246 | + | ||
| 247 | +export default withIAPContext(App) | ||
| @@ -0,0 +1,330 @@ | |||
| 1 | +import React, { Component } from 'react'; | ||
| 2 | +import { Image, ScrollView, Text, View, TextInput, StyleSheet, Button, Alert } from 'react-native'; | ||
| 3 | + | ||
| 4 | +import { base64Data } from './utils' | ||
| 5 | +import ImageEditor from '@react-native-community/image-editor'; | ||
| 6 | + | ||
| 7 | +export interface Props { | ||
| 8 | + // noop | ||
| 9 | +} | ||
| 10 | + | ||
| 11 | +interface Size { | ||
| 12 | + width: number; | ||
| 13 | + height: number; | ||
| 14 | +} | ||
| 15 | + | ||
| 16 | +interface State { | ||
| 17 | + offsetX: number | string; | ||
| 18 | + offsetY: number | string; | ||
| 19 | + sizeWidth: number | string; | ||
| 20 | + sizeHeight: number | string; | ||
| 21 | + displaySizeWidth: number | string; | ||
| 22 | + displaySizeHeight: number | string; | ||
| 23 | + resizeMode: 'contain' | 'cover' | 'stretch' | ||
| 24 | + quality: number | string; | ||
| 25 | + format?: 'png' | 'jpeg' | 'webp'; | ||
| 26 | + photoUri: any; | ||
| 27 | + photoWidth: number; | ||
| 28 | + photoHeight: number; | ||
| 29 | + croppedImageURI: string | null; | ||
| 30 | + targetSize?: Size; | ||
| 31 | + defaultType?: string; | ||
| 32 | + filePath?: string | null; | ||
| 33 | + horizontal: boolean; | ||
| 34 | + cropHorizontal: boolean; | ||
| 35 | + remoteWidth: number; | ||
| 36 | + remoteHeight: number; | ||
| 37 | +} | ||
| 38 | + | ||
| 39 | +export class ImageCropperFull extends Component<Props, State> { | ||
| 40 | + constructor(props: Props) { | ||
| 41 | + super(props); | ||
| 42 | + this.state = { | ||
| 43 | + offsetX: '0', | ||
| 44 | + offsetY: '0', | ||
| 45 | + sizeWidth: '0', | ||
| 46 | + sizeHeight: '0', | ||
| 47 | + displaySizeWidth: '0', | ||
| 48 | + displaySizeHeight: '0', | ||
| 49 | + resizeMode: 'cover', | ||
| 50 | + quality: '0.9', | ||
| 51 | + format: 'jpeg', | ||
| 52 | + photoUri: base64Data, | ||
| 53 | + photoWidth: 1080, | ||
| 54 | + photoHeight: 720, | ||
| 55 | + croppedImageURI: null, | ||
| 56 | + targetSize: { | ||
| 57 | + width: 0, height: 0 | ||
| 58 | + }, | ||
| 59 | + defaultType: 'base', | ||
| 60 | + filePath: null, | ||
| 61 | + horizontal: true, | ||
| 62 | + cropHorizontal: false, | ||
| 63 | + remoteWidth: 0, | ||
| 64 | + remoteHeight: 0 | ||
| 65 | + } | ||
| 66 | + this.remoteImage() | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + remoteImage = () => { | ||
| 70 | + Image.getSize('https://octodex.github.com/images/OctoAsians_dex_Full.png', (width, height) => { | ||
| 71 | + this.setState({ | ||
| 72 | + remoteWidth: width, | ||
| 73 | + remoteHeight: height | ||
| 74 | + }) | ||
| 75 | + }) | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + _formChange = (value, key) => { | ||
| 79 | + this.setState({ | ||
| 80 | + [key]: value | ||
| 81 | + }) | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + _changeType = (type: string): void => { | ||
| 85 | + let uri = base64Data | ||
| 86 | + let photoWidth: number = 1080 | ||
| 87 | + let photoHeight: number = 720 | ||
| 88 | + if (type === 'base') { | ||
| 89 | + uri = base64Data | ||
| 90 | + photoWidth = 1080 | ||
| 91 | + photoHeight = 720 | ||
| 92 | + } | ||
| 93 | + if (type === 'http') { | ||
| 94 | + uri = 'https://octodex.github.com/images/OctoAsians_dex_Full.png' | ||
| 95 | + photoWidth = this.state.remoteWidth || 896 | ||
| 96 | + photoHeight = this.state.remoteHeight || 896 | ||
| 97 | + } | ||
| 98 | + if (type === 'local') { | ||
| 99 | + uri = this.state.filePath | ||
| 100 | + photoWidth = this.state.targetSize?.width | ||
| 101 | + photoHeight = this.state.targetSize?.height | ||
| 102 | + } | ||
| 103 | + this.setState({ | ||
| 104 | + defaultType: type, | ||
| 105 | + photoUri: uri, | ||
| 106 | + photoWidth, | ||
| 107 | + photoHeight | ||
| 108 | + }) | ||
| 109 | + } | ||
| 110 | + | ||
| 111 | + _crop = async () => { | ||
| 112 | + let imageCropData = { | ||
| 113 | + offset: { x: parseFloat(this.state.offsetX), y: parseFloat(this.state.offsetY) }, | ||
| 114 | + size: { width: parseFloat(this.state.sizeWidth), height: parseFloat(this.state.sizeHeight) }, | ||
| 115 | + displaySize: { width: parseFloat(this.state.displaySizeWidth), height: parseFloat(this.state.displaySizeHeight) }, | ||
| 116 | + resizeMode: this.state.resizeMode, | ||
| 117 | + quality: parseFloat(this.state.quality), | ||
| 118 | + format: this.state.format, | ||
| 119 | + includeBase64: this.state.defaultType === 'base' ? true : false | ||
| 120 | + } | ||
| 121 | + if (imageCropData.size.width + imageCropData.offset.x > this.state.photoWidth || imageCropData.size.height + imageCropData.offset.y > this.state.photoHeight) { | ||
| 122 | + Alert.alert('The cropped size exceeds the original size') | ||
| 123 | + return | ||
| 124 | + } | ||
| 125 | + const cropResult = await ImageEditor.cropImage( | ||
【AI-Review】【一般】【基础代码问题】【稳定性问题】_crop 方法未捕获 ImageEditor.cropImage 异常 ● 问题: 对比同 PR 的 触发路径:用户在 ImageCropperFull 界面输入无效的裁剪参数(如 offset/size 留空导致 ● 影响:一般。当 ● 建议:使用 try-catch 包裹
![]() ![]() | |||
| 126 | + this.state.photoUri, | ||
| 127 | + imageCropData | ||
| 128 | + ); | ||
| 129 | + console.log('cropResult.uri', cropResult.uri) | ||
| 130 | + console.log('cropResult.path', cropResult.path) | ||
| 131 | + console.log('cropResult.name', cropResult.name) | ||
| 132 | + console.log('cropResult.width', cropResult.width) | ||
| 133 | + console.log('cropResult.height', cropResult.height) | ||
| 134 | + console.log('cropResult.size', cropResult.size) | ||
| 135 | + console.log('cropResult.type', cropResult.type) | ||
| 136 | + console.log('cropResult.base64', cropResult.base64) | ||
| 137 | + if (cropResult && cropResult.uri) { | ||
| 138 | + this.setState({ | ||
| 139 | + croppedImageURI: cropResult.uri, | ||
| 140 | + filePath: cropResult.uri, | ||
| 141 | + targetSize: { | ||
| 142 | + width: imageCropData.size.width, | ||
| 143 | + height: imageCropData.size.height | ||
| 144 | + } | ||
| 145 | + }); | ||
| 146 | + if (imageCropData.displaySize && imageCropData.displaySize.width && imageCropData.displaySize.height) { | ||
| 147 | + const aspect = imageCropData.size.width / imageCropData.size.height | ||
| 148 | + const targetAspect = imageCropData.displaySize.width / imageCropData.displaySize.height | ||
| 149 | + if (aspect === targetAspect || this.state.resizeMode === 'stretch' || this.state.resizeMode === 'cover') { | ||
| 150 | + this.setState({ | ||
| 151 | + targetSize: { | ||
| 152 | + width: imageCropData.displaySize.width, | ||
| 153 | + height: imageCropData.displaySize.height | ||
| 154 | + } | ||
| 155 | + }); | ||
| 156 | + } | ||
| 157 | + if (this.state.resizeMode === 'contain') { | ||
| 158 | + if (targetAspect <= aspect) { | ||
| 159 | + this.setState({ | ||
| 160 | + targetSize: { | ||
| 161 | + width: imageCropData.displaySize.width, | ||
| 162 | + height: Math.ceil(imageCropData.displaySize.width / aspect) | ||
| 163 | + } | ||
| 164 | + }); | ||
| 165 | + } else { | ||
| 166 | + this.setState({ | ||
| 167 | + targetSize: { | ||
| 168 | + height: imageCropData.displaySize.height, | ||
| 169 | + width: Math.ceil(imageCropData.displaySize.height * aspect) | ||
| 170 | + } | ||
| 171 | + }); | ||
| 172 | + } | ||
| 173 | + } | ||
| 174 | + } | ||
| 175 | + | ||
| 176 | + if (this.state.targetSize.width >= this.state.targetSize.height) { | ||
| 177 | + this.setState({ | ||
| 178 | + cropHorizontal: true | ||
| 179 | + }) | ||
| 180 | + } else { | ||
| 181 | + this.setState({ | ||
| 182 | + cropHorizontal: false | ||
| 183 | + }) | ||
| 184 | + } | ||
| 185 | + | ||
| 186 | + } | ||
| 187 | + } | ||
| 188 | + | ||
| 189 | + render() { | ||
| 190 | + const { offsetX, offsetY, sizeWidth, sizeHeight, resizeMode, quality, format, photoUri, photoWidth, photoHeight, croppedImageURI, targetSize, defaultType, displaySizeWidth, displaySizeHeight, horizontal, cropHorizontal } = this.state | ||
| 191 | + return ( | ||
| 192 | + <ScrollView> | ||
| 193 | + <Text>选择图片类型</Text> | ||
| 194 | + <View style={styles.flex}> | ||
| 195 | + <Button title="base64" onPress={() => this._changeType('base')} color={defaultType === 'base' ? 'green' : ''} /> | ||
| 196 | + <Button title="远程图片" onPress={() => this._changeType('http')} color={defaultType === 'http' ? 'green' : ''} /> | ||
| 197 | + <Button title="沙箱" onPress={() => this._changeType('local')} color={defaultType === 'local' ? 'green' : ''} /> | ||
| 198 | + </View> | ||
| 199 | + | ||
| 200 | + <ScrollView style={{ height: photoHeight }} horizontal={horizontal}> | ||
| 201 | + <Image source={{ uri: photoUri }} style={{ width: photoWidth, height: photoHeight }} /> | ||
| 202 | + </ScrollView> | ||
| 203 | + { | ||
| 204 | + croppedImageURI ? | ||
| 205 | + <ScrollView style={{ height: targetSize?.height }} horizontal={cropHorizontal}> | ||
| 206 | + <Image source={{ uri: croppedImageURI }} style={{ width: targetSize?.width, height: targetSize?.height }} /> | ||
| 207 | + </ScrollView> : | ||
| 208 | + <Text>未生成图片</Text> | ||
| 209 | + } | ||
| 210 | + | ||
| 211 | + <View style={styles.flex}> | ||
| 212 | + <Text>offset:</Text> | ||
| 213 | + <TextInput | ||
| 214 | + style={styles.inputStyle} | ||
| 215 | + value={offsetX} | ||
| 216 | + inputMode="numeric" | ||
| 217 | + placeholder="x" | ||
| 218 | + onChangeText={(data) => { | ||
| 219 | + this._formChange(data, 'offsetX') | ||
| 220 | + }} | ||
| 221 | + /> | ||
| 222 | + <TextInput | ||
| 223 | + style={styles.inputStyle} | ||
| 224 | + value={offsetY} | ||
| 225 | + inputMode="numeric" | ||
| 226 | + placeholder="y" | ||
| 227 | + onChangeText={(data) => { | ||
| 228 | + this._formChange(data, 'offsetY') | ||
| 229 | + }} | ||
| 230 | + /> | ||
| 231 | + </View> | ||
| 232 | + | ||
| 233 | + <View style={styles.flex}> | ||
| 234 | + <Text>size:</Text> | ||
| 235 | + <TextInput | ||
| 236 | + style={styles.inputStyle} | ||
| 237 | + value={sizeWidth} | ||
| 238 | + inputMode="numeric" | ||
| 239 | + placeholder="width" | ||
| 240 | + onChangeText={(data) => { | ||
| 241 | + this._formChange(data, 'sizeWidth') | ||
| 242 | + }} | ||
| 243 | + /> | ||
| 244 | + <TextInput | ||
| 245 | + style={styles.inputStyle} | ||
| 246 | + value={sizeHeight} | ||
| 247 | + inputMode="numeric" | ||
| 248 | + placeholder="height" | ||
| 249 | + onChangeText={(data) => { | ||
| 250 | + this._formChange(data, 'sizeHeight') | ||
| 251 | + }} | ||
| 252 | + /> | ||
| 253 | + </View> | ||
| 254 | + | ||
| 255 | + <View style={styles.flex}> | ||
| 256 | + <Text>displaySize:</Text> | ||
| 257 | + <TextInput | ||
| 258 | + style={styles.inputStyle} | ||
| 259 | + value={displaySizeWidth} | ||
| 260 | + inputMode="numeric" | ||
| 261 | + placeholder="width" | ||
| 262 | + onChangeText={(data) => { | ||
| 263 | + this._formChange(data, 'displaySizeWidth') | ||
| 264 | + }} | ||
| 265 | + /> | ||
| 266 | + <TextInput | ||
| 267 | + style={styles.inputStyle} | ||
| 268 | + value={displaySizeHeight} | ||
| 269 | + inputMode="numeric" | ||
| 270 | + placeholder="height" | ||
| 271 | + onChangeText={(data) => { | ||
| 272 | + this._formChange(data, 'displaySizeHeight') | ||
| 273 | + }} | ||
| 274 | + /> | ||
| 275 | + </View> | ||
| 276 | + | ||
| 277 | + <View style={styles.flex}> | ||
| 278 | + <Text>resizeMode:</Text> | ||
| 279 | + <Button title="contain" onPress={() => this._formChange('contain', 'resizeMode')} color={resizeMode === 'contain' ? 'green' : ''} /> | ||
| 280 | + <Button title="cover" onPress={() => this._formChange('cover', 'resizeMode')} color={resizeMode === 'cover' ? 'green' : ''} /> | ||
| 281 | + <Button title="stretch" onPress={() => this._formChange('stretch', 'resizeMode')} color={resizeMode === 'stretch' ? 'green' : ''} /> | ||
| 282 | + {/* <Button title="center" onPress={()=>this._formChange('center', 'resizeMode')} color={resizeMode==='center' ? 'green' : ''} /> */} | ||
| 283 | + </View> | ||
| 284 | + | ||
| 285 | + <View style={styles.flex}> | ||
| 286 | + <Text>quality:</Text> | ||
| 287 | + <Button title="0.3" onPress={() => this._formChange('0.3', 'quality')} color={quality === '0.3' ? 'green' : ''} /> | ||
| 288 | + <Button title="0.5" onPress={() => this._formChange('0.5', 'quality')} color={quality === '0.5' ? 'green' : ''} /> | ||
| 289 | + <Button title="0.9" onPress={() => this._formChange('0.9', 'quality')} color={quality === '0.9' ? 'green' : ''} /> | ||
| 290 | + <Button title="1.0" onPress={() => this._formChange('1.0', 'quality')} color={quality === '1.0' ? 'green' : ''} /> | ||
| 291 | + </View> | ||
| 292 | + | ||
| 293 | + <View style={styles.flex}> | ||
| 294 | + <Text>format:</Text> | ||
| 295 | + <Button title="jpeg" onPress={() => this._formChange('jpeg', 'format')} color={format === 'jpeg' ? 'green' : ''} /> | ||
| 296 | + <Button title="png" onPress={() => this._formChange('png', 'format')} color={format === 'png' ? 'green' : ''} /> | ||
| 297 | + <Button title="webp" onPress={() => this._formChange('webp', 'format')} color={format === 'webp' ? 'green' : ''} /> | ||
| 298 | + </View> | ||
| 299 | + | ||
| 300 | + <View style={styles.button}> | ||
| 301 | + <Text>{croppedImageURI}</Text> | ||
| 302 | + <Button title="确定" onPress={() => this._crop()} /> | ||
| 303 | + </View> | ||
| 304 | + </ScrollView> | ||
| 305 | + ); | ||
| 306 | + } | ||
| 307 | +} | ||
| 308 | + | ||
| 309 | +const styles = StyleSheet.create({ | ||
| 310 | + button: { | ||
| 311 | + padding: 10 | ||
| 312 | + }, | ||
| 313 | + inputStyle: { | ||
| 314 | + width: 120, | ||
| 315 | + height: 35, | ||
| 316 | + padding: 5, | ||
| 317 | + borderRadius: 8, | ||
| 318 | + margin: 5, | ||
| 319 | + color: 'black', | ||
| 320 | + fontSize: 12, | ||
| 321 | + borderColor: 'black', | ||
| 322 | + borderWidth: 1 | ||
| 323 | + }, | ||
| 324 | + flex: { | ||
| 325 | + display: 'flex', | ||
| 326 | + flexDirection: 'row', | ||
| 327 | + justifyContent: 'flex-start', | ||
| 328 | + alignItems: 'center' | ||
| 329 | + } | ||
| 330 | +}) | ||
| @@ -0,0 +1,25 @@ | |||
| 1 | +import React from 'react'; | ||
| 2 | + | ||
| 3 | +import { SquareImageCropper } from './SquareImageCropper'; | ||
| 4 | + | ||
| 5 | +export default function ImageEditorDemo() { | ||
| 6 | + return <SquareImageCropper />; | ||
| 7 | +} | ||
| 8 | + | ||
| 9 | + | ||
| 10 | +// 使用 export 导出 | ||
| 11 | +export const displayName = "react-native-community_image-editor"; | ||
| 12 | +export const framework = "React"; | ||
| 13 | +export const category = "Basic"; | ||
| 14 | +export const title = "react-native-community_image-editor"; | ||
| 15 | +export const documentationURL = "https://gitcode.com/CPF-RN/usage-docs/blob/br_rnoh0.61/zh-cn/react-native-image-editor.md"; | ||
| 16 | +export const description = "Simple image-editor component."; | ||
| 17 | + | ||
| 18 | +export const examples = [ | ||
| 19 | + { | ||
| 20 | + title: "image-editor with default styling", | ||
| 21 | + render: function (): any { | ||
| 22 | + return <ImageEditorDemo />; | ||
| 23 | + }, | ||
| 24 | + }, | ||
| 25 | +]; | ||
| @@ -0,0 +1,236 @@ | |||
| 1 | +import React, { Component } from 'react'; | ||
| 2 | +import { | ||
| 3 | + Image, | ||
| 4 | + StyleSheet, | ||
| 5 | + Text, | ||
| 6 | + TouchableHighlight, | ||
| 7 | + View, | ||
| 8 | + SafeAreaView, | ||
| 9 | + Button, | ||
| 10 | +} from 'react-native'; | ||
| 11 | +import ImageEditor from '@react-native-community/image-editor'; | ||
| 12 | + | ||
| 13 | +import type { LayoutChangeEvent } from 'react-native'; | ||
| 14 | +import { DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT } from './constants'; | ||
| 15 | +import { ImageCropper } from './ImageCropper'; | ||
| 16 | +import { ImageCropperFull } from './ImageCropperFull'; | ||
| 17 | +import { base64Data } from './utils'; | ||
| 18 | + | ||
| 19 | +import type { ImageCropData, ImageSize } from './types'; | ||
| 20 | + | ||
| 21 | +interface State { | ||
| 22 | + croppedImageURI: string | null; | ||
| 23 | + cropError: Error | null; | ||
| 24 | + measuredSize: ImageSize | null; | ||
| 25 | +} | ||
| 26 | +interface Props { | ||
| 27 | + // noop | ||
| 28 | +} | ||
| 29 | + | ||
| 30 | +export class SquareImageCropper extends Component<Props, State> { | ||
| 31 | + state: any; | ||
| 32 | + _isMounted: boolean; | ||
| 33 | + _transformData: ImageCropData | undefined; | ||
| 34 | + | ||
| 35 | + constructor(props: Props) { | ||
| 36 | + super(props); | ||
| 37 | + this._isMounted = true; | ||
| 38 | + this.state = { | ||
| 39 | + photo: { | ||
| 40 | + uri: base64Data, | ||
| 41 | + height: DEFAULT_IMAGE_HEIGHT, | ||
| 42 | + width: DEFAULT_IMAGE_WIDTH, | ||
| 43 | + }, | ||
| 44 | + measuredSize: null, | ||
| 45 | + croppedImageURI: null, | ||
| 46 | + cropError: null, | ||
| 47 | + demoType: 'demo1' | ||
| 48 | + }; | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + _onLayout = (event: LayoutChangeEvent) => { | ||
| 52 | + const measuredWidth = event.nativeEvent.layout.width; | ||
| 53 | + if (!measuredWidth) { | ||
| 54 | + return; | ||
| 55 | + } | ||
| 56 | + this.setState({ | ||
| 57 | + measuredSize: { width: measuredWidth, height: measuredWidth }, | ||
| 58 | + }); | ||
| 59 | + }; | ||
| 60 | + | ||
| 61 | + _onTransformDataChange = (data: ImageCropData) => { | ||
| 62 | + this._transformData = data; | ||
| 63 | + }; | ||
| 64 | + | ||
| 65 | + demoChange = (demoType) => { | ||
| 66 | + this.setState({ | ||
| 67 | + demoType | ||
| 68 | + }) | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + render() { | ||
| 72 | + if (!this.state.measuredSize) { | ||
| 73 | + return ( | ||
| 74 | + <SafeAreaView style={styles.container} onLayout={this._onLayout} /> | ||
| 75 | + ); | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + return ( | ||
| 79 | + <SafeAreaView style={styles.containerPadding}> | ||
| 80 | + <View style={styles.contentContainer}> | ||
| 81 | + { | ||
| 82 | + this.state.demoType === 'demo1' ? !this.state.croppedImageURI ? this._renderImageCropper() : this._renderCroppedImage() : '' | ||
| 83 | + } | ||
| 84 | + { | ||
| 85 | + this.state.demoType === 'demo2' ? this._renderFullDemo() : '' | ||
| 86 | + } | ||
| 87 | + </View> | ||
| 88 | + <View style={styles.buttonRow}> | ||
| 89 | + <Button title="demo1" onPress={()=>this.demoChange('demo1')} /> | ||
| 90 | + <Button title="demo2" onPress={()=>this.demoChange('demo2')} /> | ||
| 91 | + </View> | ||
| 92 | + </SafeAreaView> | ||
| 93 | + ) | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + _renderImageCropper() { | ||
| 97 | + const { photo, cropError, measuredSize } = this.state; | ||
| 98 | + | ||
| 99 | + if (!photo) { | ||
| 100 | + return <View style={styles.container} />; | ||
| 101 | + } | ||
| 102 | + | ||
| 103 | + return ( | ||
| 104 | + <View style={styles.container}> | ||
| 105 | + <Text style={styles.text} testID="headerText"> | ||
| 106 | + Drag the image within the square to crop | ||
| 107 | + </Text> | ||
| 108 | + <ImageCropper | ||
| 109 | + image={photo} | ||
| 110 | + size={measuredSize} | ||
| 111 | + style={[styles.imageCropper, measuredSize]} | ||
| 112 | + onTransformDataChange={this._onTransformDataChange} | ||
| 113 | + /> | ||
| 114 | + <TouchableHighlight | ||
| 115 | + accessibilityRole="button" | ||
| 116 | + style={styles.cropButtonTouchable} | ||
| 117 | + onPress={this._crop} | ||
| 118 | + > | ||
| 119 | + <View style={styles.cropButton}> | ||
| 120 | + <Text style={styles.cropButtonLabel}>Crop</Text> | ||
| 121 | + </View> | ||
| 122 | + </TouchableHighlight> | ||
| 123 | + <Text style={styles.errorText}>{cropError?.message}</Text> | ||
| 124 | + </View> | ||
| 125 | + ); | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + _renderCroppedImage() { | ||
| 129 | + return ( | ||
| 130 | + <View style={styles.container}> | ||
| 131 | + <Text style={styles.text}>Here is the cropped image</Text> | ||
| 132 | + <Image | ||
| 133 | + accessibilityIgnoresInvertColors | ||
| 134 | + source={{ uri: this.state.croppedImageURI }} | ||
| 135 | + style={[styles.imageCropper, this.state.measuredSize]} | ||
| 136 | + /> | ||
| 137 | + <TouchableHighlight | ||
| 138 | + accessibilityRole="button" | ||
| 139 | + style={styles.cropButtonTouchable} | ||
| 140 | + onPress={this._reset} | ||
| 141 | + > | ||
| 142 | + <View style={styles.cropButton}> | ||
| 143 | + <Text style={styles.cropButtonLabel}>Try again</Text> | ||
| 144 | + </View> | ||
| 145 | + </TouchableHighlight> | ||
| 146 | + <Text style={styles.errorText} /> | ||
| 147 | + </View> | ||
| 148 | + ); | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + _renderFullDemo() { | ||
| 152 | + return ( | ||
| 153 | + <View style={styles.container}> | ||
| 154 | + <ImageCropperFull /> | ||
| 155 | + </View> | ||
| 156 | + ) | ||
| 157 | + } | ||
| 158 | +_crop = async () => { | ||
| 159 | + try { | ||
| 160 | + if (!this._transformData) { | ||
| 161 | + return; | ||
| 162 | + } | ||
| 163 | + const cropResult = await ImageEditor.cropImage( | ||
| 164 | + this.state.photo.uri, | ||
| 165 | + this._transformData | ||
| 166 | + ); | ||
| 167 | + if (cropResult && cropResult.uri) { | ||
| 168 | + this.setState({ croppedImageURI: cropResult.uri }); | ||
| 169 | + } | ||
| 170 | + } catch (cropError) { | ||
| 171 | + if (cropError instanceof Error) { | ||
| 172 | + this.setState({ cropError }); | ||
| 173 | + } | ||
| 174 | + } | ||
| 175 | +}; | ||
| 176 | + | ||
| 177 | + _reset = () => { | ||
| 178 | + this.setState({ croppedImageURI: null, cropError: null }); | ||
| 179 | + }; | ||
| 180 | +} | ||
| 181 | + | ||
| 182 | +export default SquareImageCropper; | ||
| 183 | + | ||
| 184 | +const styles = StyleSheet.create({ | ||
| 185 | + buttonRow: { | ||
| 186 | + flexDirection: 'row', | ||
| 187 | + justifyContent: 'space-around', | ||
| 188 | + padding: 16, | ||
| 189 | + }, | ||
| 190 | + containerPadding: { | ||
| 191 | + flex: 1, | ||
| 192 | + justifyContent: 'space-between', | ||
| 193 | + paddingTop: 50, | ||
| 194 | + }, | ||
| 195 | + contentContainer: { | ||
| 196 | + flex: 1, | ||
| 197 | + justifyContent: 'center', | ||
| 198 | + alignItems: 'center', | ||
| 199 | + }, | ||
| 200 | + container: { | ||
| 201 | + flex: 1, | ||
| 202 | + backgroundColor: 'white', | ||
| 203 | + paddingTop: 0, | ||
| 204 | + alignItems: 'center', | ||
| 205 | + }, | ||
| 206 | + imageCropper: { | ||
| 207 | + alignSelf: 'center', | ||
| 208 | + marginTop: 12, | ||
| 209 | + }, | ||
| 210 | + cropButtonTouchable: { | ||
| 211 | + alignSelf: 'center', | ||
| 212 | + marginBottom: 10, | ||
| 213 | + marginTop: 'auto', | ||
| 214 | + backgroundColor: 'royalblue', | ||
| 215 | + borderRadius: 6, | ||
| 216 | + }, | ||
| 217 | + cropButton: { | ||
| 218 | + padding: 12, | ||
| 219 | + }, | ||
| 220 | + cropButtonLabel: { | ||
| 221 | + color: 'white', | ||
| 222 | + fontSize: 18, | ||
| 223 | + fontWeight: '500', | ||
| 224 | + }, | ||
| 225 | + text: { | ||
| 226 | + color: 'black', | ||
| 227 | + textAlign: 'center', | ||
| 228 | + fontSize: 16, | ||
| 229 | + }, | ||
| 230 | + errorText: { | ||
| 231 | + color: 'red', | ||
| 232 | + textAlign: 'center', | ||
| 233 | + fontSize: 16, | ||
| 234 | + marginBottom: 10, | ||
| 235 | + }, | ||
| 236 | +}); | ||
| @@ -210,7 +210,15 @@ const ComponentExamples: Array<RNTesterExample> = [ | |||
| 210 | { | 210 | { |
| 211 | key: 'react-native-drag-sort', | 211 | key: 'react-native-drag-sort', |
| 212 | module: require('../examples/react-native-drag-sort/index'), | 212 | module: require('../examples/react-native-drag-sort/index'), |
| 213 | - }, | 213 | + }, |
| 214 | + { | ||
| 215 | + key: 'react-native-iap', | ||
| 216 | + module: require('../examples/react-native-iap/index'), | ||
| 217 | + }, | ||
| 218 | + { | ||
| 219 | + key: 'react-native-image-editor', | ||
| 220 | + module: require('../examples/react-native-image-editor/ImageEditorDemo') | ||
| 221 | + }, | ||
| 214 | ]; | 222 | ]; |
| 215 | 223 | ||
| 216 | const APIExamples: Array<RNTesterExample> = []; | 224 | const APIExamples: Array<RNTesterExample> = []; |
| @@ -45,6 +45,8 @@ | |||
| 45 | "@react-native-ohos/cookies": "file:../../node_modules/@react-native-ohos/cookies/harmony/rn_cookies.har", | 45 | "@react-native-ohos/cookies": "file:../../node_modules/@react-native-ohos/cookies/harmony/rn_cookies.har", |
| 46 | "@react-native-ohos/react-native-restart": "file:../../node_modules/@react-native-ohos/react-native-restart/harmony/rn_restart.har", | 46 | "@react-native-ohos/react-native-restart": "file:../../node_modules/@react-native-ohos/react-native-restart/harmony/rn_restart.har", |
| 47 | "@react-native-ohos/react-native-exit-app": "file:../../node_modules/@react-native-ohos/react-native-exit-app/harmony/exit_app.har", | 47 | "@react-native-ohos/react-native-exit-app": "file:../../node_modules/@react-native-ohos/react-native-exit-app/harmony/exit_app.har", |
| 48 | - "@react-native-ohos/react-native-image-resizer": "file:../../node_modules/@react-native-ohos/react-native-image-resizer/harmony/image_resizer.har" | 48 | + "@react-native-ohos/react-native-image-resizer": "file:../../node_modules/@react-native-ohos/react-native-image-resizer/harmony/image_resizer.har", |
| 49 | + "@react-native-ohos/image-editor": "file:../../node_modules/@react-native-ohos/image-editor/harmony/image_editor.har", | ||
| 50 | + "@react-native-ohos/react-native-iap": "file:../../node_modules/@react-native-ohos/react-native-iap/harmony/react_native_iap.har" | ||
| 49 | } | 51 | } |
| 50 | } | 52 | } |
| @@ -55,6 +55,8 @@ add_subdirectory("${OH_MODULES}/@react-native-ohos/cookies/src/main/cpp" ./rn_co | |||
| 55 | add_subdirectory("${OH_MODULES}/@react-native-ohos/react-native-restart/src/main/cpp" ./restart) | 55 | add_subdirectory("${OH_MODULES}/@react-native-ohos/react-native-restart/src/main/cpp" ./restart) |
| 56 | add_subdirectory("${OH_MODULES}/@react-native-ohos/react-native-exit-app/src/main/cpp" ./exit_app) | 56 | add_subdirectory("${OH_MODULES}/@react-native-ohos/react-native-exit-app/src/main/cpp" ./exit_app) |
| 57 | add_subdirectory("${OH_MODULES}/@react-native-ohos/react-native-image-resizer/src/main/cpp" ./image-resizer) | 57 | add_subdirectory("${OH_MODULES}/@react-native-ohos/react-native-image-resizer/src/main/cpp" ./image-resizer) |
| 58 | +add_subdirectory("${OH_MODULES}/@react-native-ohos/react-native-iap/src/main/cpp" ./iap) | ||
| 59 | +add_subdirectory("${OH_MODULES}/@react-native-ohos/image-editor/src/main/cpp" ./image-editor) | ||
| 58 | file(GLOB GENERATED_CPP_FILES "${CMAKE_CURRENT_SOURCE_DIR}/generated/*.cpp") | 60 | file(GLOB GENERATED_CPP_FILES "${CMAKE_CURRENT_SOURCE_DIR}/generated/*.cpp") |
| 59 | add_library(rnoh_app SHARED | 61 | add_library(rnoh_app SHARED |
| 60 | ${GENERATED_CPP_FILES} | 62 | ${GENERATED_CPP_FILES} |
| @@ -104,4 +106,6 @@ target_link_libraries(rnoh_app PUBLIC rnoh_image_sequence_2) | |||
| 104 | target_link_libraries(rnoh_app PUBLIC rnoh_cookies) | 106 | target_link_libraries(rnoh_app PUBLIC rnoh_cookies) |
| 105 | target_link_libraries(rnoh_app PUBLIC rnoh_restart) | 107 | target_link_libraries(rnoh_app PUBLIC rnoh_restart) |
| 106 | target_link_libraries(rnoh_app PUBLIC rnoh_exit_app) | 108 | target_link_libraries(rnoh_app PUBLIC rnoh_exit_app) |
| 107 | -target_link_libraries(rnoh_app PUBLIC rnoh_ImageResizer) | 109 | +target_link_libraries(rnoh_app PUBLIC rnoh_ImageResizer) |
| 110 | +target_link_libraries(rnoh_app PUBLIC rnoh_iap) | ||
| 111 | +target_link_libraries(rnoh_app PUBLIC rnoh_image_editor) | ||
| @@ -48,6 +48,8 @@ | |||
| 48 | 48 | ||
| 49 | 49 | ||
| 50 | 50 | ||
| 51 | + | ||
| 52 | + | ||
| 51 | 53 | ||
| 52 | using namespace rnoh; | 54 | using namespace rnoh; |
| 53 | 55 | ||
| @@ -94,6 +96,8 @@ std::vector<std::shared_ptr<Package>> PackageProvider::getPackages(Package::Cont | |||
| 94 | std::make_shared<CookiesPackage>(ctx), | 96 | std::make_shared<CookiesPackage>(ctx), |
| 95 | std::make_shared<RestartPackage>(ctx), | 97 | std::make_shared<RestartPackage>(ctx), |
| 96 | std::make_shared<ExitAppPackage>(ctx), | 98 | std::make_shared<ExitAppPackage>(ctx), |
| 97 | - std::make_shared<ImageResizerPackage>(ctx) | 99 | + std::make_shared<ImageResizerPackage>(ctx), |
| 100 | + std::make_shared<ReactNativeIapPackage>(ctx), | ||
| 101 | + std::make_shared<ImageEditorPackage>(ctx) | ||
| 98 | }; | 102 | }; |
| 99 | } | 103 | } |
| @@ -42,6 +42,8 @@ import { CookiesPackage } from '@react-native-ohos/cookies/ts'; | |||
| 42 | import { RNRestartPackage } from '@react-native-ohos/react-native-restart/ts'; | 42 | import { RNRestartPackage } from '@react-native-ohos/react-native-restart/ts'; |
| 43 | import { ExitAppPackage } from '@react-native-ohos/react-native-exit-app/ts'; | 43 | import { ExitAppPackage } from '@react-native-ohos/react-native-exit-app/ts'; |
| 44 | import { ImageResizerPackage } from '@react-native-ohos/react-native-image-resizer/ts'; | 44 | import { ImageResizerPackage } from '@react-native-ohos/react-native-image-resizer/ts'; |
| 45 | +import { IapPackage } from '@react-native-ohos/react-native-iap/ts'; | ||
| 46 | +import { ImageEditorPackage } from '@react-native-ohos/image-editor/ts'; | ||
| 45 | 47 | ||
| 46 | export function getRNOHPackages(ctx: RNPackageContext): RNPackage[] { | 48 | export function getRNOHPackages(ctx: RNPackageContext): RNPackage[] { |
| 47 | return [ | 49 | return [ |
| @@ -79,6 +81,8 @@ export function getRNOHPackages(ctx: RNPackageContext): RNPackage[] { | |||
| 79 | new CookiesPackage(ctx), | 81 | new CookiesPackage(ctx), |
| 80 | new RNRestartPackage(ctx), | 82 | new RNRestartPackage(ctx), |
| 81 | new ExitAppPackage(ctx), | 83 | new ExitAppPackage(ctx), |
| 82 | - new ImageResizerPackage(ctx) | 84 | + new ImageResizerPackage(ctx), |
| 85 | + new IapPackage(ctx), | ||
| 86 | + new ImageEditorPackage(ctx) | ||
| 83 | ] | 87 | ] |
| 84 | } | 88 | } |
| @@ -84,7 +84,9 @@ | |||
| 84 | "@react-native-ohos/react-native-restart": "0.1.0", | 84 | "@react-native-ohos/react-native-restart": "0.1.0", |
| 85 | "@react-native-ohos/react-native-exit-app": "2.1.0", | 85 | "@react-native-ohos/react-native-exit-app": "2.1.0", |
| 86 | "@react-native-ohos/react-native-image-resizer": "3.1.0", | 86 | "@react-native-ohos/react-native-image-resizer": "3.1.0", |
| 87 | - "@react-native-ohos/react-native-drag-sort": "2.5.1-rc.1" | 87 | + "@react-native-ohos/react-native-drag-sort": "2.5.1-rc.1", |
| 88 | + "@react-native-ohos/react-native-iap": "13.0.5", | ||
| 89 | + "@react-native-ohos/image-editor": "4.3.1" | ||
| 88 | }, | 90 | }, |
| 89 | "devDependencies": { | 91 | "devDependencies": { |
| 90 | "@babel/core": "^7.25.2", | 92 | "@babel/core": "^7.25.2", |


【AI-Review】【严重】【基础代码问题】【代码逻辑错误】getProducts/getSubscriptions 参数 skus 嵌套数组导致查询失败
● 问题:在
getItemsByType方法中,productIds已经是string[]类型(由this.state.skuIds.split(',').map(s => s.trim()).filter(Boolean)生成),但构造params时使用了const params = { skus: [productIds] },多包了一层数组。这会导致params.skus变成string[][]嵌套数组(例如[['sku_001', 'sku_002']]),而不是 react-native-iap API 期望的string[](即['sku_001', 'sku_002'])。根据 react-native-iap 的 API 定义(src/iap.ts),
getProducts的签名为({ skus: string[] }): Promise<Array<Product>>,且入口校验if (!skus?.length)会通过(因为[['sku_001', 'sku_002']].length === 1),但实际传递给鸿蒙原生模块Iap.getProducts({skus})的是嵌套数组,原生模块无法正确解析 SKU 列表。● 影响:严重。当用户点击"按类型获取商品"按钮时(默认
skuIds为'sku_001,sku_002'),由于params.skus是[['sku_001', 'sku_002']]而非['sku_001', 'sku_002'],原生模块接收到的不是字符串数组而是嵌套数组,导致商品查询失败或返回空结果,测试用例的核心功能无法正常运行。同一 PR 的
test.tsx中getSubscriptions({ skus: ['SubProduct01001'] })和getProducts({ skus: ['ohos_consume_002'] })的正确用法证明了 API 期望skus为string[]而非string[][]。● 建议:移除多余的数组包装,直接使用
productIds:const params = { skus: productIds };