Skip to content

Commit 492b854

Browse files
committed
feat(wip): sponsors page
1 parent 6b19230 commit 492b854

File tree

8 files changed

+773
-110
lines changed

8 files changed

+773
-110
lines changed

app.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export default ({config}: ConfigContext): ExpoConfig => ({
4040
'expo-build-properties',
4141
{
4242
// https://github.com/software-mansion/react-native-screens/issues/2219
43-
ios: {newArchEnabled: true},
43+
ios: {newArchEnabled: true, deploymentTarget: '15.0'},
4444
android: {newArchEnabled: true},
4545
},
4646
],

app/(app)/settings/index.tsx

+30
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import useAppState from '../../../src/hooks/useAppState';
2020
import {openEmail} from '../../../src/utils/common';
2121
import ErrorBoundary from 'react-native-error-boundary';
2222
import FallbackComponent from '../../../src/components/uis/FallbackComponent';
23+
import {showAlert} from '../../../src/utils/alert';
2324

2425
const Container = styled.View`
2526
background-color: ${({theme}) => theme.bg.basic};
@@ -121,6 +122,35 @@ export default function Settings(): JSX.Element {
121122
),
122123
title: t('settings.updateProfile'),
123124
},
125+
{
126+
onPress: () => {
127+
if (Platform.OS !== 'web') {
128+
push('/settings/sponsors');
129+
return;
130+
}
131+
132+
showAlert(t('error.notSupportedInWeb'));
133+
},
134+
startElement: (
135+
<Icon
136+
name="HeartStraight"
137+
size={24}
138+
style={css`
139+
margin-right: 16px;
140+
`}
141+
/>
142+
),
143+
endElement: (
144+
<Icon
145+
name="CaretRight"
146+
size={16}
147+
style={css`
148+
margin-left: auto;
149+
`}
150+
/>
151+
),
152+
title: t('settings.sponsors'),
153+
},
124154
{
125155
onPress: () => push('/settings/block-users'),
126156
startElement: (

app/(app)/settings/sponsors.tsx

+270
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import {
2+
endConnection,
3+
getProducts,
4+
getSubscriptions,
5+
initConnection,
6+
isProductAndroid,
7+
isProductIos,
8+
isSubscriptionProductAndroid,
9+
isSubscriptionProductIos,
10+
purchaseErrorListener,
11+
purchaseUpdatedListener,
12+
requestPurchase,
13+
requestSubscription,
14+
} from 'expo-iap';
15+
import type {
16+
Product,
17+
ProductPurchase,
18+
PurchaseError,
19+
SubscriptionProduct,
20+
} from 'expo-iap/build/ExpoIap.types';
21+
import type {RequestSubscriptionAndroidProps} from 'expo-iap/build/types/ExpoIapAndroid.types';
22+
import {Stack} from 'expo-router';
23+
import {useEffect, useState} from 'react';
24+
import {
25+
Alert,
26+
Button,
27+
InteractionManager,
28+
Pressable,
29+
SafeAreaView,
30+
ScrollView,
31+
StyleSheet,
32+
Text,
33+
View,
34+
} from 'react-native';
35+
import {t} from '../../../src/STRINGS';
36+
37+
const productSkus = [
38+
'cpk.points.200',
39+
'cpk.points.500',
40+
'cpk.points.1000',
41+
'cpk.points.5000',
42+
'cpk.points.10000',
43+
'cpk.points.30000',
44+
];
45+
46+
const subscriptionSkus = [
47+
'cpk.membership.monthly.bronze',
48+
'cpk.membership.monthly.silver',
49+
];
50+
51+
const operations = [
52+
'initConnection',
53+
'getProducts',
54+
'getSubscriptions',
55+
'endConnection',
56+
];
57+
type Operation = (typeof operations)[number];
58+
59+
export default function App() {
60+
const [isConnected, setIsConnected] = useState(false);
61+
const [products, setProducts] = useState<Product[]>([]);
62+
const [subscriptions, setSubscriptions] = useState<SubscriptionProduct[]>([]);
63+
64+
const handleOperation = async (operation: Operation) => {
65+
switch (operation) {
66+
case 'initConnection':
67+
if (await initConnection()) setIsConnected(true);
68+
return;
69+
70+
case 'endConnection':
71+
if (await endConnection()) {
72+
setProducts([]);
73+
setIsConnected(false);
74+
}
75+
break;
76+
77+
case 'getProducts':
78+
try {
79+
const products = await getProducts(productSkus);
80+
setProducts(products);
81+
} catch (error) {
82+
console.error(error);
83+
}
84+
break;
85+
86+
case 'getSubscriptions':
87+
try {
88+
const subscriptions = await getSubscriptions(subscriptionSkus);
89+
setSubscriptions(subscriptions);
90+
} catch (error) {
91+
console.error(error);
92+
}
93+
break;
94+
95+
default:
96+
console.log('Unknown operation');
97+
}
98+
};
99+
100+
useEffect(() => {
101+
const purchaseUpdatedSubs = purchaseUpdatedListener(
102+
(purchase: ProductPurchase) => {
103+
InteractionManager.runAfterInteractions(() => {
104+
Alert.alert('Purchase updated', JSON.stringify(purchase));
105+
});
106+
},
107+
);
108+
109+
const purchaseErrorSubs = purchaseErrorListener((error: PurchaseError) => {
110+
InteractionManager.runAfterInteractions(() => {
111+
Alert.alert('Purchase error', JSON.stringify(error));
112+
});
113+
});
114+
115+
return () => {
116+
purchaseUpdatedSubs.remove();
117+
purchaseErrorSubs.remove();
118+
endConnection();
119+
};
120+
}, []);
121+
122+
return (
123+
<SafeAreaView style={styles.container}>
124+
<Stack.Screen options={{title: t('settings.sponsors')}} />
125+
<Text style={styles.title}>Expo IAP Example</Text>
126+
<View style={styles.buttons}>
127+
<ScrollView contentContainerStyle={styles.buttonsWrapper} horizontal>
128+
{operations.map((operation) => (
129+
<Pressable
130+
key={operation}
131+
onPress={() => handleOperation(operation)}
132+
>
133+
<View style={styles.buttonView}>
134+
<Text>{operation}</Text>
135+
</View>
136+
</Pressable>
137+
))}
138+
</ScrollView>
139+
</View>
140+
<View style={styles.content}>
141+
{!isConnected ? (
142+
<Text>Not connected</Text>
143+
) : (
144+
<View style={{gap: 12}}>
145+
<Text style={{fontSize: 20}}>Products</Text>
146+
{products.map((item) => {
147+
if (isProductAndroid(item)) {
148+
return (
149+
<View key={item.title} style={{gap: 12}}>
150+
<Text>
151+
{item.title} -{' '}
152+
{item.oneTimePurchaseOfferDetails?.formattedPrice}
153+
</Text>
154+
<Button
155+
title="Buy"
156+
onPress={() => {
157+
requestPurchase({
158+
skus: [item.productId],
159+
});
160+
}}
161+
/>
162+
</View>
163+
);
164+
}
165+
166+
if (isProductIos(item)) {
167+
return (
168+
<View key={item.id} style={{gap: 12}}>
169+
<Text>
170+
{item.displayName} - {item.displayPrice}
171+
</Text>
172+
<Button
173+
title="Buy"
174+
onPress={() => {
175+
requestPurchase({
176+
sku: item.id,
177+
});
178+
}}
179+
/>
180+
</View>
181+
);
182+
}
183+
})}
184+
185+
<Text style={{fontSize: 20}}>Subscriptions</Text>
186+
{subscriptions.map((item) => {
187+
if (isSubscriptionProductAndroid(item)) {
188+
return item.subscriptionOfferDetails?.map((offer) => (
189+
<View key={offer.offerId} style={{gap: 12}}>
190+
<Text>
191+
{item.title} -{' '}
192+
{offer.pricingPhases.pricingPhaseList
193+
.map((ppl) => ppl.billingPeriod)
194+
.join(',')}
195+
</Text>
196+
<Button
197+
title="Subscribe"
198+
onPress={() => {
199+
requestSubscription({
200+
skus: [item.productId],
201+
...(offer.offerToken && {
202+
subscriptionOffers: [
203+
{
204+
sku: item.productId,
205+
offerToken: offer.offerToken,
206+
},
207+
],
208+
}),
209+
} as RequestSubscriptionAndroidProps);
210+
}}
211+
/>
212+
</View>
213+
));
214+
}
215+
216+
if (isSubscriptionProductIos(item)) {
217+
return (
218+
<View key={item.id} style={{gap: 12}}>
219+
<Text>
220+
{item.displayName} - {item.displayPrice}
221+
</Text>
222+
<Button
223+
title="Subscribe"
224+
onPress={() => {
225+
requestSubscription({sku: item.id});
226+
}}
227+
/>
228+
</View>
229+
);
230+
}
231+
})}
232+
</View>
233+
)}
234+
</View>
235+
</SafeAreaView>
236+
);
237+
}
238+
239+
const styles = StyleSheet.create({
240+
container: {
241+
flex: 1,
242+
backgroundColor: '#fff',
243+
alignItems: 'center',
244+
},
245+
title: {
246+
marginTop: 24,
247+
fontSize: 20,
248+
fontWeight: 'bold',
249+
},
250+
buttons: {
251+
height: 90,
252+
},
253+
buttonsWrapper: {
254+
padding: 24,
255+
256+
gap: 8,
257+
},
258+
buttonView: {
259+
borderRadius: 8,
260+
borderWidth: 1,
261+
borderColor: '#000',
262+
padding: 8,
263+
},
264+
content: {
265+
flex: 1,
266+
alignSelf: 'stretch',
267+
padding: 24,
268+
gap: 12,
269+
},
270+
});

assets/langs/en.json

+2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"failedToFetchData": "Failed to fetch data",
7676
"failedToGetPushToken": "Failed to get push token for push notification!",
7777
"mustUsePhysicalDeviceForSubject": "Must use physical device for {{subject}}",
78+
"notSupportedInWeb": "This feature is not supported in web",
7879
"projectIdNotFound": "Project ID not found",
7980
"unableToOpenEmailClient": "Unable to open email client"
8081
},
@@ -144,6 +145,7 @@
144145
"darkMode": "Dark Mode",
145146
"loginInfo": "Login Information",
146147
"notificationSettings": "Notification Settings",
148+
"sponsors": "Sponsors",
147149
"termsOfService": "Terms of Service",
148150
"title": "Settings",
149151
"updateProfile": "Update Profile"

assets/langs/ko.json

+2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"failedToFetchData": "데이터를 가져오는 데 실패했습니다",
7676
"failedToGetPushToken": "푸시 토큰을 가져오는 데 실패했습니다",
7777
"mustUsePhysicalDeviceForSubject": "{{subject}}을(를) 위해 실제 기기를 사용해야 합니다",
78+
"notSupportedInWeb": "웹에서는 지원하지 않는 기능입니다.",
7879
"projectIdNotFound": "프로젝트 ID를 찾을 수 없습니다",
7980
"unableToOpenEmailClient": "이메일 클라이언트를 열 수 없습니다"
8081
},
@@ -144,6 +145,7 @@
144145
"darkMode": "다크 모드",
145146
"loginInfo": "로그인 정보",
146147
"notificationSettings": "알림 설정",
148+
"sponsors": "후원사",
147149
"termsOfService": "서비스 약관",
148150
"title": "설정",
149151
"updateProfile": "프로필 수정"

eas.json

+13-1
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,22 @@
1111
"simulator": true
1212
}
1313
},
14+
"preview": {
15+
"channel": "preview",
16+
"extends": "production",
17+
"distribution": "internal",
18+
"android": {
19+
"buildType": "apk"
20+
}
21+
},
1422
"production": {
1523
"env": {
1624
"ENVIRONMENT": "production"
25+
},
26+
"channel": "production",
27+
"cache": {
28+
"disabled": true
1729
}
1830
}
1931
}
20-
}
32+
}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"expo-device": "~6.0.2",
5959
"expo-file-system": "^17.0.1",
6060
"expo-font": "~12.0.9",
61+
"expo-iap": "^2.0.0-rc.2",
6162
"expo-image": "~1.12.13",
6263
"expo-image-picker": "~15.0.7",
6364
"expo-linear-gradient": "^13.0.2",

0 commit comments

Comments
 (0)