From aacb4f87318b5dd6647ebddbd3ac9c6cb73b9856 Mon Sep 17 00:00:00 2001 From: jjbrunton Date: Fri, 28 Aug 2026 19:26:53 +0100 Subject: [PATCH 1/8] docs(subscription): correct stale Android RevenueCat status The Platform Support section claimed Android was "not yet implemented". Android has full native SDK support: getApiKey() selects the Android key, com.android.vending.BILLING is declared in app.json, the paywall has no platform gating, and the webhook passes event.store through unchanged so PLAY_STORE events need no separate handling. Also flag the entitlement ID mismatch between the "pro" fallback in revenuecat.ts and the "Sauci Pro" value set in eas.json. A build profile missing that variable yields isProUser: false with no error, which presents as churn rather than as misconfiguration. Co-Authored-By: Claude Opus 5 --- docs/subscription-system.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/subscription-system.md b/docs/subscription-system.md index 82545a6..f39ad9b 100644 --- a/docs/subscription-system.md +++ b/docs/subscription-system.md @@ -28,7 +28,10 @@ The `RevenueCatService` class wraps the `react-native-purchases` SDK. **Platform Support:** - iOS: Full support with native SDK -- Android: Not yet implemented +- Android: Full support with native SDK. `getApiKey()` selects the Android key, + `com.android.vending.BILLING` is declared in `app.json`, and the paywall has no + platform gating. The backend is store-agnostic: the webhook passes `event.store` + through unchanged, so `PLAY_STORE` events need no separate handling. - Web/Expo Go: Gracefully disabled (returns defaults) **Key Methods:** @@ -201,11 +204,18 @@ The `get_recommended_questions()` function also filters premium packs: |----------|----------|---------| | `EXPO_PUBLIC_REVENUECAT_IOS_API_KEY` | Mobile app | RevenueCat iOS public SDK key | | `EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY` | Mobile app | RevenueCat Android public SDK key | -| `EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID` | Mobile app | Entitlement identifier (default: "pro") | +| `EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID` | Mobile app | Entitlement identifier (code default: `"pro"`; builds set `"Sauci Pro"` — see warning below) | | `REVENUECAT_API_KEY` | Edge functions | RevenueCat secret API key | | `REVENUECAT_ENTITLEMENT_ID` | Edge functions | Server-side entitlement check | | `REVENUECAT_WEBHOOK_SECRET` | Edge functions | Webhook auth verification | +> **Entitlement ID mismatch risk.** The fallback in `src/lib/revenuecat.ts` is +> `"pro"`, but `eas.json` sets `EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID` to +> `"Sauci Pro"`. If that variable is missing from a build profile, the SDK looks up +> an entitlement that does not exist, `parseCustomerInfo()` returns +> `isProUser: false`, and paying users silently appear unsubscribed with no error. +> Confirm the variable is set on every iOS and Android build profile. + ## Subscription Flow Diagrams ### Purchase Flow From f35114dc7767d75b506263e363f6b5ad4bc2bdbc Mon Sep 17 00:00:00 2001 From: jjbrunton Date: Fri, 28 Aug 2026 19:54:05 +0100 Subject: [PATCH 2/8] chore(android): target API 36 with Expo 54 Upgrade the mobile runtime and tracked Android project to compile and target API 36. Align Expo modules, React Native, Node, Gradle, and Android build tooling while preserving the custom widget sources.\n\nAdapt file-system, notification, animation, test, and Metro compatibility for the SDK 54 runtime. --- .nvmrc | 2 +- apps/admin/package.json | 4 +- apps/mobile/android/app/build.gradle | 27 +- .../android/app/src/main/AndroidManifest.xml | 6 +- .../java/com/sauci/app/MainApplication.kt | 15 +- .../app/src/main/res/values/styles.xml | 1 + apps/mobile/android/build.gradle | 21 +- apps/mobile/android/gradle.properties | 18 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- apps/mobile/android/settings.gradle | 49 +- apps/mobile/app.json | 9 +- apps/mobile/app/(app)/_layout.tsx | 2 +- apps/mobile/eas.json | 6 +- apps/mobile/metro.config.js | 32 +- apps/mobile/package.json | 87 +- .../src/__tests__/hooks/useMediaSaver.test.ts | 4 +- .../components/share/SharePreviewModal.tsx | 2 +- .../IntensitySlider/IntensitySliderTrack.tsx | 4 +- .../components/DrawingShareModal.tsx | 2 +- .../live-draw/hooks/useCanvasCapture.ts | 2 +- apps/mobile/src/hooks/useAvatarPicker.ts | 2 +- apps/mobile/src/hooks/useMediaSaver.ts | 2 +- apps/mobile/src/lib/imageCache.ts | 2 +- apps/mobile/src/lib/mediaApi.ts | 2 +- apps/mobile/src/lib/notifications.ts | 33 +- apps/mobile/src/test/setup.ts | 15 +- apps/web/package.json | 4 +- package-lock.json | 9323 +++++++---------- package.json | 16 +- scripts/fix-hoisting.js | 23 + 30 files changed, 4072 insertions(+), 5645 deletions(-) diff --git a/.nvmrc b/.nvmrc index d5a1596..f5b3751 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.10.0 +20.19.4 diff --git a/apps/admin/package.json b/apps/admin/package.json index 6dcd995..7680d32 100755 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -44,8 +44,8 @@ "tailwind-merge": "^2.2.0" }, "devDependencies": { - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", + "@types/react": "~19.1.10", + "@types/react-dom": "~19.1.7", "@vitejs/plugin-react": "^4.2.0", "autoprefixer": "^10.4.16", "postcss": "^8.4.32", diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index 7dadd75..9abf19e 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -14,6 +14,7 @@ react { hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc" codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() + enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean() // Use Expo CLI to bundle the app, this ensures the Metro config // works correctly with Expo projects. cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim()) @@ -63,9 +64,9 @@ react { } /** - * Set this to true to Run Proguard on Release builds to minify the Java bytecode. + * Set this to true in release builds to optimize the app using R8. */ -def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean() +def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBuilds') ?: false).toBoolean() /** * The preferred build flavor of JavaScriptCore (JSC) @@ -78,7 +79,7 @@ def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInRelea * give correct results when using with locales other than en-US. Note that * this variant is about 6MiB larger per architecture than default. */ -def jscFlavor = 'org.webkit:android-jsc:+' +def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+' android { ndkVersion rootProject.ext.ndkVersion @@ -93,6 +94,7 @@ android { targetSdkVersion rootProject.ext.targetSdkVersion versionCode 46 versionName "1.0.5" + buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\"" } signingConfigs { debug { @@ -110,15 +112,18 @@ android { // Caution! In production, you need to generate your own keystore file. // see https://reactnative.dev/docs/signed-apk-android. signingConfig signingConfigs.debug - shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false) - minifyEnabled enableProguardInReleaseBuilds + def enableShrinkResources = findProperty('android.enableShrinkResourcesInReleaseBuilds') ?: 'false' + shrinkResources enableShrinkResources.toBoolean() + minifyEnabled enableMinifyInReleaseBuilds proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" - crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true) + def enablePngCrunchInRelease = findProperty('android.enablePngCrunchInReleaseBuilds') ?: 'true' + crunchPngs enablePngCrunchInRelease.toBoolean() } } packagingOptions { jniLibs { - useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false) + def enableLegacyPackaging = findProperty('expo.useLegacyPackaging') ?: 'false' + useLegacyPackaging enableLegacyPackaging.toBoolean() } } androidResources { @@ -156,15 +161,15 @@ dependencies { if (isGifEnabled) { // For animated gif support - implementation("com.facebook.fresco:animated-gif:${reactAndroidLibs.versions.fresco.get()}") + implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}") } if (isWebpEnabled) { // For webp support - implementation("com.facebook.fresco:webpsupport:${reactAndroidLibs.versions.fresco.get()}") + implementation("com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}") if (isWebpAnimatedEnabled) { // Animated webp support - implementation("com.facebook.fresco:animated-webp:${reactAndroidLibs.versions.fresco.get()}") + implementation("com.facebook.fresco:animated-webp:${expoLibs.versions.fresco.get()}") } } @@ -176,4 +181,4 @@ dependencies { } apply plugin: 'com.google.gms.google-services' -apply plugin: 'com.google.firebase.crashlytics' \ No newline at end of file +apply plugin: 'com.google.firebase.crashlytics' diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml index 0895015..36c65e1 100644 --- a/apps/mobile/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,10 @@ + + + + @@ -17,7 +21,7 @@ - + diff --git a/apps/mobile/android/app/src/main/java/com/sauci/app/MainApplication.kt b/apps/mobile/android/app/src/main/java/com/sauci/app/MainApplication.kt index 644f608..d691313 100644 --- a/apps/mobile/android/app/src/main/java/com/sauci/app/MainApplication.kt +++ b/apps/mobile/android/app/src/main/java/com/sauci/app/MainApplication.kt @@ -8,10 +8,10 @@ import com.facebook.react.ReactApplication import com.facebook.react.ReactNativeHost import com.facebook.react.ReactPackage import com.facebook.react.ReactHost -import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load +import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint import com.facebook.react.defaults.DefaultReactNativeHost -import com.facebook.react.soloader.OpenSourceMergedSoMapping -import com.facebook.soloader.SoLoader +import com.facebook.react.common.ReleaseLevel import expo.modules.ApplicationLifecycleDispatcher import expo.modules.ReactNativeHostWrapper @@ -42,11 +42,12 @@ class MainApplication : Application(), ReactApplication { override fun onCreate() { super.onCreate() - SoLoader.init(this, OpenSourceMergedSoMapping) - if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { - // If you opted-in for the New Architecture, we load the native entry point for this app. - load() + DefaultNewArchitectureEntryPoint.releaseLevel = try { + ReleaseLevel.valueOf(BuildConfig.REACT_NATIVE_RELEASE_LEVEL.uppercase()) + } catch (e: IllegalArgumentException) { + ReleaseLevel.STABLE } + loadReactNative(this) ApplicationLifecycleDispatcher.onApplicationCreate(this) } diff --git a/apps/mobile/android/app/src/main/res/values/styles.xml b/apps/mobile/android/app/src/main/res/values/styles.xml index b226501..85c39a6 100644 --- a/apps/mobile/android/app/src/main/res/values/styles.xml +++ b/apps/mobile/android/app/src/main/res/values/styles.xml @@ -1,5 +1,6 @@