umpordez 1.3.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +9 -1
- package/cli.mjs +1 -1
- package/create.mjs +194 -116
- package/package.json +1 -1
- package/template/AGENTS.md +1 -1
- package/template/CLAUDE.md +6 -5
- package/template/README.md +4 -4
- package/template/doc/index.html +5 -2
- package/template/mobile/{{PROJECT_SLUG}}/CLAUDE.md +18 -6
- package/template/mobile/{{PROJECT_SLUG}}/app.json +14 -34
- package/template/mobile/{{PROJECT_SLUG}}/eslint.config.js +12 -1
- package/template/mobile/{{PROJECT_SLUG}}/native/README.md +3 -3
- package/template/mobile/{{PROJECT_SLUG}}/package.json +22 -22
- package/template/mobile/{{PROJECT_SLUG}}/src/components/AnimatedSplash.tsx +10 -2
- package/template/mobile/{{PROJECT_SLUG}}/src/components/PremiumGate.tsx +4 -1
- package/template/mobile/{{PROJECT_SLUG}}/src/config.ts +21 -0
- package/template/mobile/{{PROJECT_SLUG}}/src/pages/auth/AuthLanding.tsx +55 -50
- package/template/mobile/{{PROJECT_SLUG}}/src/services/content/signing.ts +2 -1
- package/template/mobile/{{PROJECT_SLUG}}/src/services/iap/index.ts +17 -22
- package/template-variables.mjs +183 -0
- package/update.mjs +52 -48
- package/template/mobile/{{PROJECT_SLUG}}/src/services/health/index.ts +0 -238
|
@@ -7,24 +7,11 @@
|
|
|
7
7
|
"orientation": "portrait",
|
|
8
8
|
"icon": "./assets/icon.png",
|
|
9
9
|
"userInterfaceStyle": "automatic",
|
|
10
|
-
"splash": {
|
|
11
|
-
"image": "./assets/splash-icon.png",
|
|
12
|
-
"resizeMode": "contain",
|
|
13
|
-
"backgroundColor": "{{PRIMARY_COLOR}}"
|
|
14
|
-
},
|
|
15
10
|
"ios": {
|
|
16
11
|
"supportsTablet": false,
|
|
17
|
-
"bundleIdentifier": "{{IOS_BUNDLE_ID}}",
|
|
18
|
-
"usesAppleSignIn": true,
|
|
19
|
-
"entitlements": {
|
|
20
|
-
"com.apple.developer.healthkit": true
|
|
21
|
-
},
|
|
12
|
+
"bundleIdentifier": "{{IOS_BUNDLE_ID}}",{{IOS_APPLE_SIGNIN_JSON}}
|
|
22
13
|
"infoPlist": {
|
|
23
|
-
"ITSAppUsesNonExemptEncryption": false
|
|
24
|
-
"NSHealthShareUsageDescription":
|
|
25
|
-
"{{PROJECT_NAME}} reads activity data to keep your fitness summaries accurate.",
|
|
26
|
-
"NSHealthUpdateUsageDescription":
|
|
27
|
-
"{{PROJECT_NAME}} writes session data so it shows up in Apple Health alongside your other apps."
|
|
14
|
+
"ITSAppUsesNonExemptEncryption": false
|
|
28
15
|
}
|
|
29
16
|
},
|
|
30
17
|
"android": {
|
|
@@ -35,14 +22,22 @@
|
|
|
35
22
|
"backgroundColor": "{{PRIMARY_COLOR}}"
|
|
36
23
|
},
|
|
37
24
|
"permissions": [
|
|
38
|
-
"android.permission.INTERNET"
|
|
39
|
-
"com.android.vending.BILLING"
|
|
25
|
+
"android.permission.INTERNET"{{IAP_BILLING_PERMISSION_JSON}}
|
|
40
26
|
]
|
|
41
27
|
},
|
|
42
28
|
"web": {
|
|
43
29
|
"favicon": "./assets/favicon.png"
|
|
44
30
|
},
|
|
45
31
|
"plugins": [
|
|
32
|
+
[
|
|
33
|
+
"expo-splash-screen",
|
|
34
|
+
{
|
|
35
|
+
"image": "./assets/splash-icon.png",
|
|
36
|
+
"imageWidth": 200,
|
|
37
|
+
"resizeMode": "contain",
|
|
38
|
+
"backgroundColor": "{{PRIMARY_COLOR}}"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
46
41
|
[
|
|
47
42
|
"expo-build-properties",
|
|
48
43
|
{
|
|
@@ -54,23 +49,8 @@
|
|
|
54
49
|
}
|
|
55
50
|
}
|
|
56
51
|
],
|
|
57
|
-
"expo-web-browser",
|
|
58
|
-
"expo-
|
|
59
|
-
"expo-sqlite",
|
|
60
|
-
"react-native-iap",
|
|
61
|
-
[
|
|
62
|
-
"react-native-health",
|
|
63
|
-
{
|
|
64
|
-
"healthSharePermission":
|
|
65
|
-
"{{PROJECT_NAME}} reads activity data to keep your fitness summaries accurate.",
|
|
66
|
-
"healthUpdatePermission":
|
|
67
|
-
"{{PROJECT_NAME}} writes session data so it shows up in Apple Health alongside your other apps."
|
|
68
|
-
}
|
|
69
|
-
],
|
|
70
|
-
[
|
|
71
|
-
"./plugins/withAndroidHealthConnect",
|
|
72
|
-
{ "permissions": ["READ_STEPS", "WRITE_EXERCISE"] }
|
|
73
|
-
]
|
|
52
|
+
"expo-web-browser",{{APPLE_AUTH_PLUGIN_JSON}}
|
|
53
|
+
"expo-sqlite"{{IAP_PLUGIN_JSON}}
|
|
74
54
|
],
|
|
75
55
|
"extra": {
|
|
76
56
|
"eas": {
|
|
@@ -11,7 +11,18 @@ export default [
|
|
|
11
11
|
rules: {
|
|
12
12
|
"@stylistic/indent": ["error", 4],
|
|
13
13
|
"@stylistic/semi": ["error", "always"],
|
|
14
|
-
|
|
14
|
+
// ignorePattern: SVG path data (brand icons) can't be
|
|
15
|
+
// wrapped without corrupting the artwork.
|
|
16
|
+
"@stylistic/max-len": ["error", {
|
|
17
|
+
"code": 80,
|
|
18
|
+
"ignorePattern": "\\sd=\""
|
|
19
|
+
}],
|
|
20
|
+
// Underscore prefix = intentionally unused (e.g. a screen
|
|
21
|
+
// that takes navigation props but doesn't read them yet).
|
|
22
|
+
"@typescript-eslint/no-unused-vars": ["error", {
|
|
23
|
+
"argsIgnorePattern": "^_",
|
|
24
|
+
"varsIgnorePattern": "^_"
|
|
25
|
+
}],
|
|
15
26
|
"curly": ["error", "all"]
|
|
16
27
|
}
|
|
17
28
|
}
|
|
@@ -89,9 +89,9 @@ these are auto-installed** — copy out what you need:
|
|
|
89
89
|
`scripts/prebuild.sh` refuses `--clean`. To genuinely reset, move
|
|
90
90
|
customizations into config plugins under `./plugins/` first.
|
|
91
91
|
|
|
92
|
-
- **
|
|
93
|
-
(e.g.
|
|
94
|
-
`try/require` so Expo Go doesn't blow up. See
|
|
92
|
+
- **Native modules + Expo Go = crash on import.** Anything whose
|
|
93
|
+
native side isn't in Expo Go (e.g. expo-iap) needs to be
|
|
94
|
+
lazy-loaded with `try/require` so Expo Go doesn't blow up. See
|
|
95
95
|
`src/services/iap/index.ts` for the pattern.
|
|
96
96
|
|
|
97
97
|
- **iOS HealthKit needs an entitlement.** It's wired in `app.json`
|
|
@@ -12,49 +12,49 @@
|
|
|
12
12
|
"lint": "eslint src --ext .ts,.tsx",
|
|
13
13
|
"typecheck": "tsc --noEmit"
|
|
14
14
|
},
|
|
15
|
+
"expo": {
|
|
16
|
+
"install": {
|
|
17
|
+
"exclude": ["typescript"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
15
20
|
"dependencies": {
|
|
16
21
|
"@noble/hashes": "^2.2.0",
|
|
17
|
-
"@react-native-async-storage/async-storage": "
|
|
22
|
+
"@react-native-async-storage/async-storage": "2.2.0",
|
|
18
23
|
"@react-navigation/bottom-tabs": "^7.7.3",
|
|
19
24
|
"@react-navigation/native": "^7.1.19",
|
|
20
25
|
"@react-navigation/native-stack": "^7.6.2",
|
|
21
|
-
"expo": "~57.0.
|
|
26
|
+
"expo": "~57.0.4",
|
|
22
27
|
"expo-apple-authentication": "~57.0.0",
|
|
23
|
-
"expo-auth-session": "~
|
|
28
|
+
"expo-auth-session": "~57.0.2",
|
|
24
29
|
"expo-build-properties": "~57.0.3",
|
|
25
30
|
"expo-constants": "~57.0.3",
|
|
26
|
-
"expo-file-system": "~57.0.0",
|
|
27
|
-
"expo-linking": "~57.0.
|
|
31
|
+
"expo-file-system": "~57.0.0",{{IAP_DEP_JSON}}
|
|
32
|
+
"expo-linking": "~57.0.2",
|
|
28
33
|
"expo-localization": "~57.0.0",
|
|
29
|
-
"expo-share-intent": "^8.0.0",
|
|
30
34
|
"expo-splash-screen": "~57.0.2",
|
|
31
35
|
"expo-sqlite": "~57.0.0",
|
|
32
36
|
"expo-status-bar": "~57.0.0",
|
|
33
37
|
"expo-system-ui": "~57.0.0",
|
|
34
38
|
"expo-web-browser": "~57.0.0",
|
|
35
39
|
"i18next": "^23.16.5",
|
|
36
|
-
"lucide-react-native": "^1.
|
|
37
|
-
"react": "19.
|
|
38
|
-
"react-native": "0.
|
|
40
|
+
"lucide-react-native": "^1.24.0",
|
|
41
|
+
"react": "19.2.3",
|
|
42
|
+
"react-native": "0.86.0",
|
|
39
43
|
"react-i18next": "^15.1.1",
|
|
40
|
-
"react-native-gesture-handler": "~
|
|
41
|
-
"react-native-
|
|
42
|
-
"react-native-
|
|
43
|
-
"react-native-
|
|
44
|
-
"react-native-
|
|
45
|
-
"react-native-reanimated": "~4.5.1",
|
|
46
|
-
"react-native-safe-area-context": "^5.8.0",
|
|
47
|
-
"react-native-screens": "~4.25.2",
|
|
48
|
-
"react-native-svg": "15.12.1",
|
|
44
|
+
"react-native-gesture-handler": "~2.32.0",
|
|
45
|
+
"react-native-reanimated": "4.5.0",
|
|
46
|
+
"react-native-safe-area-context": "~5.7.0",
|
|
47
|
+
"react-native-screens": "4.25.2",
|
|
48
|
+
"react-native-svg": "15.15.4",
|
|
49
49
|
"react-native-toast-message": "^2.3.3",
|
|
50
|
-
"react-native-worklets": "
|
|
50
|
+
"react-native-worklets": "0.10.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@stylistic/eslint-plugin": "^2.13.0",
|
|
54
|
-
"@types/react": "~19.
|
|
55
|
-
"babel-preset-expo": "~57.0.
|
|
54
|
+
"@types/react": "~19.2.2",
|
|
55
|
+
"babel-preset-expo": "~57.0.2",
|
|
56
56
|
"eslint": "^9.20.1",
|
|
57
|
-
"typescript": "~5.
|
|
57
|
+
"typescript": "~5.9.3",
|
|
58
58
|
"typescript-eslint": "^8.24.0"
|
|
59
59
|
}
|
|
60
60
|
}
|
|
@@ -25,6 +25,10 @@ const { width, height } = Dimensions.get('window');
|
|
|
25
25
|
const BG = DARK.bg;
|
|
26
26
|
const GLOW_COLOR = DARK.primary;
|
|
27
27
|
|
|
28
|
+
// Static asset — require() is the RN idiom for bundled images.
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
30
|
+
const SPLASH_ICON = require('../../assets/splash-icon.png');
|
|
31
|
+
|
|
28
32
|
// Glow + ring sit at screen-centre because our generated splash-icon
|
|
29
33
|
// is a trimmed glyph composited onto a transparent square canvas (see
|
|
30
34
|
// icons.mjs renderFromScratch — splash mode). With resizeMode:contain
|
|
@@ -177,7 +181,7 @@ export default function AnimatedSplash({ onDone }: Props) {
|
|
|
177
181
|
/>
|
|
178
182
|
|
|
179
183
|
<Animated.Image
|
|
180
|
-
source={
|
|
184
|
+
source={SPLASH_ICON}
|
|
181
185
|
style={[
|
|
182
186
|
s.splash,
|
|
183
187
|
{ opacity: logoOp, transform: [{ scale: logoSc }] }
|
|
@@ -193,7 +197,11 @@ const RING_SIZE = width * 0.58;
|
|
|
193
197
|
|
|
194
198
|
const s = StyleSheet.create({
|
|
195
199
|
root: {
|
|
196
|
-
|
|
200
|
+
position: 'absolute',
|
|
201
|
+
top: 0,
|
|
202
|
+
left: 0,
|
|
203
|
+
right: 0,
|
|
204
|
+
bottom: 0,
|
|
197
205
|
backgroundColor: BG,
|
|
198
206
|
alignItems: 'center',
|
|
199
207
|
justifyContent: 'center',
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from 'react-native';
|
|
8
8
|
import { Lock } from 'lucide-react-native';
|
|
9
9
|
import { useTranslation } from 'react-i18next';
|
|
10
|
+
import config from '../config';
|
|
10
11
|
import { useAuth } from '../contexts/AuthContext';
|
|
11
12
|
import { useTheme } from '../contexts/ThemeContext';
|
|
12
13
|
|
|
@@ -39,7 +40,9 @@ export default function PremiumGate({
|
|
|
39
40
|
const finalDescription =
|
|
40
41
|
description ?? t('premiumGate.defaultDescription');
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
// Projects scaffolded without in-app purchases have nothing to
|
|
44
|
+
// sell — every feature renders unlocked.
|
|
45
|
+
if (!config.iap || user?.is_premium) {
|
|
43
46
|
return <>{children}</>;
|
|
44
47
|
}
|
|
45
48
|
|
|
@@ -47,6 +47,24 @@ const GOOGLE_IOS_CLIENT_ID = 'TODO_GOOGLE_IOS_CLIENT_ID';
|
|
|
47
47
|
// appears. Set at scaffold time via `umpordez create`.
|
|
48
48
|
export const SPLASH_ANIMATED = {{SPLASH_ANIMATED}};
|
|
49
49
|
|
|
50
|
+
// Sign-in providers, chosen at scaffold time via `umpordez create`.
|
|
51
|
+
// Email+password always ships; these gate the extra buttons on
|
|
52
|
+
// AuthLanding. Flipping appleAuth back on also requires restoring
|
|
53
|
+
// `usesAppleSignIn` + the `expo-apple-authentication` plugin in
|
|
54
|
+
// app.json (the scaffold strips them when Apple is disabled so iOS
|
|
55
|
+
// builds don't demand the Sign in with Apple capability).
|
|
56
|
+
const GOOGLE_AUTH_ENABLED = {{AUTH_GOOGLE_ENABLED}};
|
|
57
|
+
const APPLE_AUTH_ENABLED = {{AUTH_APPLE_ENABLED}};
|
|
58
|
+
|
|
59
|
+
// In-app purchases, chosen at scaffold time via `umpordez create`.
|
|
60
|
+
// When disabled the expo-iap dependency, config plugin and Android
|
|
61
|
+
// BILLING permission were stripped from the scaffold — PremiumGate
|
|
62
|
+
// renders its children unlocked and the iap service no-ops. To turn
|
|
63
|
+
// IAP on later: `npx expo install expo-iap`, add "expo-iap" to
|
|
64
|
+
// app.json plugins, re-add com.android.vending.BILLING, restore the
|
|
65
|
+
// try/require in src/services/iap/index.ts and flip this to true.
|
|
66
|
+
const IAP_ENABLED = {{IAP_ENABLED}};
|
|
67
|
+
|
|
50
68
|
if (__DEV__) {
|
|
51
69
|
console.log(
|
|
52
70
|
`[config] API: ${baseApiUrl} | prod=${isProdBuild} `
|
|
@@ -62,5 +80,8 @@ export default {
|
|
|
62
80
|
version: APP_VERSION,
|
|
63
81
|
googleClientId: GOOGLE_CLIENT_ID,
|
|
64
82
|
googleIosClientId: GOOGLE_IOS_CLIENT_ID,
|
|
83
|
+
googleAuth: GOOGLE_AUTH_ENABLED,
|
|
84
|
+
appleAuth: APPLE_AUTH_ENABLED,
|
|
85
|
+
iap: IAP_ENABLED,
|
|
65
86
|
scheme: '{{MOBILE_SCHEME}}'
|
|
66
87
|
};
|
|
@@ -47,7 +47,7 @@ export default function AuthLandingScreen({ navigation }: Props) {
|
|
|
47
47
|
})
|
|
48
48
|
]).start();
|
|
49
49
|
|
|
50
|
-
if (Platform.OS === 'ios') {
|
|
50
|
+
if (Platform.OS === 'ios' && config.appleAuth) {
|
|
51
51
|
AppleAuthentication.isAvailableAsync()
|
|
52
52
|
.then(setAppleAvailable)
|
|
53
53
|
.catch(() => setAppleAvailable(false));
|
|
@@ -279,58 +279,63 @@ export default function AuthLandingScreen({ navigation }: Props) {
|
|
|
279
279
|
</TouchableOpacity>
|
|
280
280
|
)}
|
|
281
281
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
s.googleBtn,
|
|
285
|
-
{
|
|
286
|
-
backgroundColor: colors.card,
|
|
287
|
-
borderColor: colors.border
|
|
288
|
-
},
|
|
289
|
-
busy && s.btnDisabled
|
|
290
|
-
]}
|
|
291
|
-
onPress={handleGoogle}
|
|
292
|
-
activeOpacity={0.88}
|
|
293
|
-
disabled={busy}
|
|
294
|
-
>
|
|
295
|
-
{provider === 'google' ? (
|
|
296
|
-
<ActivityIndicator color={colors.text} />
|
|
297
|
-
) : (
|
|
298
|
-
<>
|
|
299
|
-
<GoogleIcon />
|
|
300
|
-
<Text
|
|
301
|
-
style={[
|
|
302
|
-
s.googleTxt,
|
|
303
|
-
{ color: colors.text }
|
|
304
|
-
]}
|
|
305
|
-
>
|
|
306
|
-
{t('auth.landing.google')}
|
|
307
|
-
</Text>
|
|
308
|
-
</>
|
|
309
|
-
)}
|
|
310
|
-
</TouchableOpacity>
|
|
311
|
-
|
|
312
|
-
<View style={s.divRow}>
|
|
313
|
-
<View
|
|
314
|
-
style={[
|
|
315
|
-
s.divLine,
|
|
316
|
-
{ backgroundColor: colors.border }
|
|
317
|
-
]}
|
|
318
|
-
/>
|
|
319
|
-
<Text
|
|
282
|
+
{config.googleAuth && (
|
|
283
|
+
<TouchableOpacity
|
|
320
284
|
style={[
|
|
321
|
-
s.
|
|
322
|
-
{
|
|
285
|
+
s.googleBtn,
|
|
286
|
+
{
|
|
287
|
+
backgroundColor: colors.card,
|
|
288
|
+
borderColor: colors.border
|
|
289
|
+
},
|
|
290
|
+
busy && s.btnDisabled
|
|
323
291
|
]}
|
|
292
|
+
onPress={handleGoogle}
|
|
293
|
+
activeOpacity={0.88}
|
|
294
|
+
disabled={busy}
|
|
324
295
|
>
|
|
325
|
-
{
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
296
|
+
{provider === 'google' ? (
|
|
297
|
+
<ActivityIndicator color={colors.text} />
|
|
298
|
+
) : (
|
|
299
|
+
<>
|
|
300
|
+
<GoogleIcon />
|
|
301
|
+
<Text
|
|
302
|
+
style={[
|
|
303
|
+
s.googleTxt,
|
|
304
|
+
{ color: colors.text }
|
|
305
|
+
]}
|
|
306
|
+
>
|
|
307
|
+
{t('auth.landing.google')}
|
|
308
|
+
</Text>
|
|
309
|
+
</>
|
|
310
|
+
)}
|
|
311
|
+
</TouchableOpacity>
|
|
312
|
+
)}
|
|
313
|
+
|
|
314
|
+
{(config.googleAuth
|
|
315
|
+
|| (Platform.OS === 'ios' && appleAvailable)) && (
|
|
316
|
+
<View style={s.divRow}>
|
|
317
|
+
<View
|
|
318
|
+
style={[
|
|
319
|
+
s.divLine,
|
|
320
|
+
{ backgroundColor: colors.border }
|
|
321
|
+
]}
|
|
322
|
+
/>
|
|
323
|
+
<Text
|
|
324
|
+
style={[
|
|
325
|
+
s.divLabel,
|
|
326
|
+
{ color: colors.textMuted }
|
|
327
|
+
]}
|
|
328
|
+
>
|
|
329
|
+
{t('auth.landing.divider')}
|
|
330
|
+
</Text>
|
|
331
|
+
<View
|
|
332
|
+
style={[
|
|
333
|
+
s.divLine,
|
|
334
|
+
{ backgroundColor: colors.border }
|
|
335
|
+
]}
|
|
336
|
+
/>
|
|
337
|
+
</View>
|
|
338
|
+
)}
|
|
334
339
|
|
|
335
340
|
<TouchableOpacity
|
|
336
341
|
style={[
|
|
@@ -19,7 +19,8 @@ import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js';
|
|
|
19
19
|
|
|
20
20
|
// CHANGE THIS to a long random string and keep it in sync with
|
|
21
21
|
// server/.env CONTENT_SECRET. Same value at both ends.
|
|
22
|
-
export const CONTENT_SECRET =
|
|
22
|
+
export const CONTENT_SECRET =
|
|
23
|
+
'{{CONTENT_SECRET}}';
|
|
23
24
|
|
|
24
25
|
interface SignedHeaders {
|
|
25
26
|
'X-Content-Timestamp': string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* In-App Purchase service — wraps `
|
|
2
|
+
* In-App Purchase service — wraps `expo-iap` (OpenIAP) with:
|
|
3
3
|
*
|
|
4
|
-
* - Lazy-load via try/require so Expo Go (no
|
|
4
|
+
* - Lazy-load via try/require so Expo Go (no native module) gets
|
|
5
5
|
* graceful no-ops instead of crashing on import.
|
|
6
6
|
* - One source of truth for product IDs (PRODUCT_IDS below — change
|
|
7
7
|
* them to match what you set up in App Store Connect / Play Console).
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { Platform } from 'react-native';
|
|
16
|
+
import config from '../../config';
|
|
16
17
|
import { fetchApi } from '../fetch-api';
|
|
17
18
|
import type { ApiUser } from '../api';
|
|
18
19
|
|
|
@@ -35,22 +36,13 @@ interface IAPNative {
|
|
|
35
36
|
isConsumable: boolean;
|
|
36
37
|
}) => Promise<unknown>;
|
|
37
38
|
getAvailablePurchases: () => Promise<NativePurchase[]>;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}) => Promise<unknown>;
|
|
39
|
+
deepLinkToSubscriptions?: (args?: {
|
|
40
|
+
packageNameAndroid?: string;
|
|
41
|
+
skuAndroid?: string;
|
|
42
|
+
} | null) => Promise<unknown>;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
48
|
-
_iap = require('react-native-iap') as IAPNative;
|
|
49
|
-
} catch {
|
|
50
|
-
console.info(
|
|
51
|
-
'[iap] react-native-iap not available (Expo Go) — IAP disabled'
|
|
52
|
-
);
|
|
53
|
-
}
|
|
45
|
+
{{IAP_REQUIRE_BLOCK}}
|
|
54
46
|
|
|
55
47
|
const isAvailable = !!_iap;
|
|
56
48
|
|
|
@@ -60,8 +52,8 @@ const isAvailable = !!_iap;
|
|
|
60
52
|
// product IDs) and Google Play Console (Monetize → Subscriptions). The
|
|
61
53
|
// strings can be anything but must match across all three places.
|
|
62
54
|
export const PRODUCT_IDS = {
|
|
63
|
-
monthly: '{{
|
|
64
|
-
annual: '{{
|
|
55
|
+
monthly: '{{PROJECT_SLUG_UNDERSCORE}}_premium_monthly',
|
|
56
|
+
annual: '{{PROJECT_SLUG_UNDERSCORE}}_premium_annual'
|
|
65
57
|
} as const;
|
|
66
58
|
|
|
67
59
|
const PRODUCT_ID_LIST = Object.values(PRODUCT_IDS);
|
|
@@ -314,7 +306,7 @@ export async function openSubscriptionManagement(): Promise<boolean> {
|
|
|
314
306
|
}
|
|
315
307
|
try {
|
|
316
308
|
if (Platform.OS === 'ios') {
|
|
317
|
-
await _iap!.
|
|
309
|
+
await _iap!.deepLinkToSubscriptions?.();
|
|
318
310
|
return true;
|
|
319
311
|
}
|
|
320
312
|
await _iap!.deepLinkToSubscriptions?.({
|
|
@@ -350,9 +342,9 @@ export async function validateWithServer(
|
|
|
350
342
|
): Promise<ValidateResponse> {
|
|
351
343
|
let body: Record<string, unknown>;
|
|
352
344
|
if (Platform.OS === 'ios') {
|
|
353
|
-
// StoreKit 2 /
|
|
354
|
-
//
|
|
355
|
-
//
|
|
345
|
+
// StoreKit 2 / expo-iap no longer exposes a receipt blob —
|
|
346
|
+
// server validates via transactionId using the App Store
|
|
347
|
+
// Server API.
|
|
356
348
|
const originalTransactionId =
|
|
357
349
|
purchase.originalTransactionIdentifierIOS
|
|
358
350
|
?? purchase.transactionId;
|
|
@@ -397,6 +389,9 @@ export async function fetchPremiumStatus(token: string): Promise<{
|
|
|
397
389
|
ok: boolean;
|
|
398
390
|
is_premium: boolean;
|
|
399
391
|
}> {
|
|
392
|
+
if (!config.iap) {
|
|
393
|
+
return { ok: false, is_premium: false };
|
|
394
|
+
}
|
|
400
395
|
try {
|
|
401
396
|
return await fetchApi('/api/iap/status', { token });
|
|
402
397
|
} catch (err) {
|