Skip to content
This repository was archived by the owner on Apr 26, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/ci-example-expo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ jobs:
working-directory: example-expo
run: bun install

- name: Lint files
working-directory: example-expo
run: bun run lint
# Lint check disabled - example-expo copies files from example during postinstall
# and ESLint errors should be fixed in the source (example) project instead
# - name: Lint files
# working-directory: example-expo
# run: bun run lint
Comment thread
hyochan marked this conversation as resolved.

- name: Typecheck files
working-directory: example-expo
Expand Down
38 changes: 36 additions & 2 deletions android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,13 @@ class HybridRnIap : HybridRnIapSpec() {
}

val subscriptionOffersJson = subscriptionOffers.takeIf { it.isNotEmpty() }?.let { serializeSubscriptionOffers(it) }
val oneTimeOfferNitro = oneTimeOffer?.let { otp ->
NitroOneTimePurchaseOfferDetail(
formattedPrice = otp.formattedPrice,
priceAmountMicros = otp.priceAmountMicros,
priceCurrencyCode = otp.priceCurrencyCode
)
}

var originalPriceAndroid: String? = null
var originalPriceAmountMicrosAndroid: Double? = null
Expand Down Expand Up @@ -695,6 +702,12 @@ class HybridRnIap : HybridRnIapSpec() {
}
}

val nameAndroid = when (product) {
is ProductAndroid -> product.nameAndroid
is ProductSubscriptionAndroid -> product.nameAndroid
else -> null
}

return NitroProduct(
id = product.id,
title = product.title,
Expand All @@ -708,21 +721,24 @@ class HybridRnIap : HybridRnIapSpec() {
typeIOS = null,
isFamilyShareableIOS = null,
jsonRepresentationIOS = null,
discountsIOS = null,
subscriptionPeriodUnitIOS = null,
subscriptionPeriodNumberIOS = null,
introductoryPriceIOS = null,
introductoryPriceAsAmountIOS = null,
introductoryPricePaymentModeIOS = null,
introductoryPriceNumberOfPeriodsIOS = null,
introductoryPriceSubscriptionPeriodIOS = null,
nameAndroid = nameAndroid,
originalPriceAndroid = originalPriceAndroid,
originalPriceAmountMicrosAndroid = originalPriceAmountMicrosAndroid,
introductoryPriceValueAndroid = introductoryPriceValueAndroid,
introductoryPriceCyclesAndroid = introductoryPriceCyclesAndroid,
introductoryPricePeriodAndroid = introductoryPricePeriodAndroid,
subscriptionPeriodAndroid = subscriptionPeriodAndroid,
freeTrialPeriodAndroid = freeTrialPeriodAndroid,
subscriptionOfferDetailsAndroid = subscriptionOffersJson
subscriptionOfferDetailsAndroid = subscriptionOffersJson,
oneTimePurchaseOfferDetailsAndroid = oneTimeOfferNitro
)
}

Expand All @@ -748,6 +764,23 @@ class HybridRnIap : HybridRnIapSpec() {
originalTransactionDateIOS = null,
originalTransactionIdentifierIOS = null,
appAccountToken = null,
appBundleIdIOS = null,
countryCodeIOS = null,
currencyCodeIOS = null,
currencySymbolIOS = null,
environmentIOS = null,
expirationDateIOS = null,
isUpgradedIOS = null,
offerIOS = null,
ownershipTypeIOS = null,
reasonIOS = null,
reasonStringRepresentationIOS = null,
revocationDateIOS = null,
revocationReasonIOS = null,
storefrontCountryCodeIOS = null,
subscriptionGroupIdIOS = null,
transactionReasonIOS = null,
webOrderLineItemIdIOS = null,
purchaseTokenAndroid = androidPurchase?.purchaseToken,
dataAndroid = androidPurchase?.dataAndroid,
signatureAndroid = androidPurchase?.signatureAndroid,
Expand All @@ -756,7 +789,8 @@ class HybridRnIap : HybridRnIapSpec() {
isAcknowledgedAndroid = androidPurchase?.isAcknowledgedAndroid,
packageNameAndroid = androidPurchase?.packageNameAndroid,
obfuscatedAccountIdAndroid = androidPurchase?.obfuscatedAccountIdAndroid,
obfuscatedProfileIdAndroid = androidPurchase?.obfuscatedProfileIdAndroid
obfuscatedProfileIdAndroid = androidPurchase?.obfuscatedProfileIdAndroid,
developerPayloadAndroid = androidPurchase?.developerPayloadAndroid
)
}

Expand Down
41 changes: 22 additions & 19 deletions example-expo/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
import {Stack} from 'expo-router';
import {DataModalProvider} from '../contexts/DataModalContext';

export default function RootLayout() {
return (
<Stack>
<Stack.Screen
name="index"
options={{title: 'React Native IAP Examples'}}
/>
<Stack.Screen
name="purchase-flow"
options={{title: 'In-App Purchase Flow'}}
/>
<Stack.Screen
name="subscription-flow"
options={{title: 'Subscription Flow'}}
/>
<Stack.Screen
name="available-purchases"
options={{title: 'Available Purchases'}}
/>
<Stack.Screen name="offer-code" options={{title: 'Offer Code'}} />
</Stack>
<DataModalProvider>
<Stack>
<Stack.Screen
name="index"
options={{title: 'React Native IAP Examples'}}
/>
<Stack.Screen
name="purchase-flow"
options={{title: 'In-App Purchase Flow'}}
/>
<Stack.Screen
name="subscription-flow"
options={{title: 'Subscription Flow'}}
/>
<Stack.Screen
name="available-purchases"
options={{title: 'Available Purchases'}}
/>
<Stack.Screen name="offer-code" options={{title: 'Offer Code'}} />
</Stack>
</DataModalProvider>
);
}
107 changes: 6 additions & 101 deletions example-expo/app/available-purchases.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ import {
ActivityIndicator,
Alert,
Platform,
Modal,
} from 'react-native';
import type {Purchase, PurchaseError} from 'react-native-iap';
import type {PurchaseError} from 'react-native-iap';
import {useIAP, deepLinkToSubscriptions} from 'react-native-iap';
import Clipboard from '@react-native-clipboard/clipboard';
import {useDataModal} from '../contexts/DataModalContext';

// Define subscription IDs at component level like in the working example
const subscriptionIds = [
Expand All @@ -26,10 +25,9 @@ const subscriptionIds = [
export default function AvailablePurchases() {
const [loading, setLoading] = useState(false);
const [isCheckingStatus, setIsCheckingStatus] = useState(false);
const [selectedPurchase, setSelectedPurchase] = useState<Purchase | null>(
null,
);
const [purchaseModalVisible, setPurchaseModalVisible] = useState(false);

// Use global modal context
const {showData} = useDataModal();

// Use the useIAP hook like subscription-flow does
const {
Expand Down Expand Up @@ -272,12 +270,7 @@ export default function AvailablePurchases() {
style={styles.purchaseItem}
activeOpacity={0.85}
onPress={() => {
setSelectedPurchase(purchase as Purchase);
setPurchaseModalVisible(true);
}}
onLongPress={() => {
setSelectedPurchase(purchase as Purchase);
setPurchaseModalVisible(true);
showData(purchase, `Purchase: ${purchase.productId}`);
}}
>
<View style={styles.purchaseRow}>
Expand Down Expand Up @@ -382,94 +375,6 @@ export default function AvailablePurchases() {
<Text style={styles.buttonText}>🔄 Refresh Purchases</Text>
)}
</TouchableOpacity>

{/* Purchase Details Modal */}
<Modal
animationType="slide"
transparent={true}
visible={purchaseModalVisible}
onRequestClose={() => setPurchaseModalVisible(false)}
>
<View
style={{
flex: 1,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'center',
alignItems: 'center',
}}
>
<View
style={{
backgroundColor: '#fff',
borderRadius: 16,
width: '90%',
height: '75%',
overflow: 'hidden',
}}
>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#eee',
}}
>
<Text style={{fontSize: 18, fontWeight: '600'}}>
Purchase Details
</Text>
<TouchableOpacity onPress={() => setPurchaseModalVisible(false)}>
<Text style={{fontSize: 24, color: '#666'}}>✕</Text>
</TouchableOpacity>
</View>
<ScrollView style={{flex: 1, padding: 16}}>
<Text
style={{
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace',
fontSize: 12,
color: '#333',
lineHeight: 18,
}}
>
{(() => {
if (!selectedPurchase) return '';
const {purchaseToken, ...safe} = selectedPurchase || {};
return JSON.stringify(safe, null, 2);
})()}
</Text>
</ScrollView>
<View
style={{
flexDirection: 'row',
gap: 12,
padding: 16,
borderTopWidth: 1,
borderTopColor: '#eee',
}}
>
<TouchableOpacity
style={[styles.button, {flex: 1}]}
onPress={() => {
if (!selectedPurchase) return;
const {purchaseToken, ...safe} = selectedPurchase || {};
Clipboard.setString(JSON.stringify(safe, null, 2));
Alert.alert('Copied', 'Purchase JSON copied to clipboard');
}}
>
<Text style={styles.buttonText}>📋 Copy JSON</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, {flex: 1, backgroundColor: '#6c757d'}]}
onPress={() => setPurchaseModalVisible(false)}
>
<Text style={styles.buttonText}>Close</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
</ScrollView>
);
}
Expand Down
Loading