Notes on learning stage of developing my own React Native education app using Expo Framework - Part 6

Last updated on 20 Nov. 2025
Minor update on 22 Nov. 2025


Main tutorial used for learning: Simon Grimm: Build Your Own MASTERCLASS clone in React Native, https://www.youtube.com/watch?v=fO3D8lNs10c , 4 hrs. 42 mins, Mar. 2025.





Jump to Details section (an easy way to get to just after end of main notes section and then scroll up to see latest main notes in this post).

=======================================
14:03, 10 November 2025

Restarting SG Edu app work after a break of around 3 weeks.


I have a development deployment ...

lms> eas env:push --path .env
√ Select environment: » development
Uploaded env file to development.
lms> 
...

I ran: ...
lms> eas deploy
...
🚀 When you are ready to deploy to production:
  $ eas deploy --prod
lms> 
----

On expo.dev, all env variables are defined only for development. No env vars defined for production.

So if I want to create a production web deployment, I need to do:
1) set up production env variables on expo.dev, say by using 'eas env:push --path .env' and choosing production as environment
2) Run eas deploy --prod

On expo.dev, in Hosting deployments, I could not find a direct indication/field about whether the deployment is production or development. But choosing the vertical three dots menu for both my current deployments, shows an option to 'Promote to production'. That indicates that they are development deployments.

I was able to login to https://sg-eduapp-lms--st52bu6bp9.expo.app/ . Except for 'Browse Courses' button, the data in the pages remained blank for quite some time but eventually both images and text were shown. I could see the lessons videos too. Once again longish wait for data (perhaps 1 minute or so). 
Could the wait have been so long due to break of around 20 days since I have used the app  (and its STRAPI backend)? [Update: Reason seems to be Cold Start for STRAPI free tier ... more later on in this post.]
The My Studio, Browse Courses and My Courses screens do not have loading spinners.
But the Lessons page has loading spinners/message for the drawer as well as the main screen
----------------

Old To Dos which are still not done:

*) After above, production deploy can be considered. With prod. deploy change below env var in .env.cloud.fs:
# EXPO_PUBLIC_WEB_SOON_URL=http://10.50.171.151:8081/soon
*) In Browse courses -> particular course -> course page and in Course overview page:
The image at the top gets vertically clipped by a large amount when window width is large like on PC desktop full window width case. It is not a big issue but if the same solution to above issue which was applied in home page can be easily applied here, it may be worth trying it out.
*) In Mobile Android app: Browse Courses screen extends into status bar. [But Course Overview screen does not do so.]
*) In Mobile Android app, Browse Courses screen can be dragged down to close the screen. That seems to be the only screen which has this feature. Is it something SG used on this screen alone to demonstrate the feature? In web app, Browse Courses screen cannot be dragged (mouse click-and-drag) in any direction.
------------

I think that excluding the first To Do which is not an issue, the other 'Old To Dos' issues are minor. I need not spend time on trying to fix them.

[Update: Done.] But loading spinner for My Studio, Browse Courses and My Courses screens would be nice to have.

===========
Me: 2025-11-10 17:44:36 I am restarting this work after a break of 20 odd days.
On expo.dev, I am trying to figure out the app.json used for the last deployment. But I don't see an app.json listed there.

CG: Expo does not upload your app.json to expo.dev as a separate visible file.
This is expected — the dashboard never shows an app.json entry. ....

[Tried some CG suggestions. Did not work.]

Me: In short, the developer has to track the app.json used for a particular seq. of:
expo export --platform web
eas deploy
====
using a git commit for that.

Am I right?

CG: ✅ Yes — you are 100% right.

For the Expo workflow you are using:

expo export --platform web eas deploy

with:

"web": { "bundler": "metro", "output": "server" }

the only reliable way to know which exact app.json was used for a deployment is:

✅ Track app.json via Git commits

(or any other version control system)

...
=====================

git commit -m "Probably the app.json used for https://sg-eduapp-lms--st52bu6bp9.expo.app/ deployment"
---

Me: Will the app.json changes we made in last entries of:
    "web": {
      "bundler": "metro",
      "output": "server",
      "favicon": "./assets/images/favicon.png",
      "display": "standalone",
      "themeColor": "#0d6c9a",
      "backgroundColor": "#ffffff",
      "shortName": "LMS",
      "startUrl": "."
    },
----
which were not effective for our PWA attempt, create any issues for regular usage of web app?
Should I keep the app.json as above or revert to the earlier version?

CG: Short answer: It is safe to keep the new app.json entries. They will not cause any issues for normal web app usage.
...

[So I have decided to not revert app.json to earlier version.]

[I switched to a new chat, "Loading indicator issue".]
Me: In a React Native and Expo project which has Android and web app versions, I am not seeing a loading/activity indicator in some screens including the main screen. I will share two related source files code with you.
...
Me: I am following a tutorial. So this is not my code. I am trying to understand it and fix it if there are errors. Please note this.
...
Me: First question, why has he set IsLoading to false initially itself for web platform. any idea?

CG: Great question — and the answer is important because it reveals a misunderstanding in the tutorial code.

Why did the tutorial set isLoading to false for Web?

The line:

const [isLoading, setIsLoading] = useState(Platform.OS === 'web' ? false : true);

means:

  • On Web: do NOT show the loader

  • On Android/iOS: show the loader until onLoadEnd fires

Based on the patterns I’ve seen in similar tutorials, the likely reasons are:


1. The tutorial author assumed Web loads instantly

On Web, React Query often returns cached data immediately.
So the author may have thought:

“Web apps load faster; no need for a spinner.”

But this assumption is wrong, because:

  • initial load still takes time,

  • network latency affects web too,

  • React Query may not have cached data.

So hiding the loader on Web leads to a bad UX.


2. They intended to use dom.onLoadEnd — but used it incorrectly

They expected:

  • On Android/iOS: native <Image> and <ScrollView> fire load events

  • On Web: expo/dom handles load events differently

  • So they set isLoading=false on Web so it depends only on React Query loading.

However:

✅ They never passed the dom prop into <ScrollView> or <Image>
✅ So onLoadEnd() never fires anywhere
✅ And isLoading becomes meaningless

This is likely an oversight in the tutorial.

.....


✅ Summary

The tutorial author probably set isLoading=false on Web because:

✅ They assumed Web loads instantly
✅ They wanted the Android/iOS splash to depend on image/DOM load
❌ But they made a mistake: dom.onLoadEnd is never used
✅ So they hid the loader on Web to avoid a permanent spinner
In short:

📌 It’s a workaround for broken code — not a good design choice.


...

Me: Some more background before I ask you for solutions. The backend is a STRAPI server hosted on STRAPI cloud. I am on free tier and both on web and Android app (development build), the STRAPI data requests are slow at initial app load time. So clearly an Activity Indicator or at least a 'Loading ...' message must be shown. Interestingly, in the lessons part of the application, a 'Loading...' text message does get shown.

Coming back to the Home screen issue, yes, I need activity/loading indicator for both web and Android. BTW the 'use dom' part does not create an issue for HomeBlock.tsx to work in Android (after delay for first load, the screen loads quite fine on Android showing image and text from STRAPI backend correctly). Now please guide me in solution for both web and Android.

CG: ...

✅ Use React Query’s built-in loading state

This is the only reliable and cross-platform way to handle loading.

Replace your query with:

const { data, isLoading, isFetching, error } = useQuery({ queryKey: ['homeInfo'], queryFn: getHomeInfo, });

isLoading → first load

isFetching → background refetch

✔ Works on Web + Android + iOS

✔ No need for Platform.OS === 'web' ? false : true

...
Me: I want to keep the code changes minimal as I want to stay close to the tutorial but fix the issues. So I think I can skip the background fetching related part and also the error part.

...
Me: I think its working now on both Android and web. Great!
You suggested:
          <Text className="mt-4 text-base">Loading...</Text>
in addition to Activity Indicator.
I think that's a good idea as the activity indicator currently is quite small in size though centered. Pls comment

CG: ...

And yes, adding a text message like “Loading…” is a very good idea, especially in your specific situation. Here’s why:


Why adding “Loading…” is helpful

1. ActivityIndicator is very small by default

  • On Android, the default spinner is quite small (even with large screens).

  • On Web, React Native Web renders a CSS spinner that is even smaller.

  • Users may miss it, especially on first app load where Strapi is slow.

Adding text makes it much more visible.


2. Text improves user confidence

A spinning indicator alone can feel like the app is stuck.
But text like “Loading…” reassures users that:

✔ the app is working
✔ data is being fetched
✔ there’s no error

This is especially important when Strapi (free tier) has slow first response times.

...
Me: This is working well enough for my current needs (demo with clear message about it being tutorial level):
      {isLoading && (
        <View className="flex-1 items-center justify-center">
          <ActivityIndicator size="large" />
          <Text className="text-black dark:text-white">Loading...</Text>
          <Text className="text-black dark:text-white">
            Due to free-tier backend, response, especially first response, may be slow.
          </Text>
        </View>
      )}
...

git commit -m "Fixed activity indicator issue in Home screen for both web and Android"

[Did similar work for other screens after creating a reusable activity component ....]

git commit -m "Added LoadingView component; Fixed activity indicator issue in Browse Courses and My Courses related screens"

============

Me: That works. Great.

As per Strapi web page, Strapi free tier's runtime behaviour is 'Cold start' whereas the paid tiers are 'Always on'. But even then, my first responses, say after web app refresh, is taking close to 1 minute, at times. After first response, for newly visited screens, it is much faster. there still is small delay at times, but it is quite small with the loading message appearing for only few seconds (less than 10 typically).
I am a little suprised by close to 1 minute first response even for a free tier. What's ur view?

CG: Here’s the realistic picture of what’s happening with Strapi Cloud Free Tier and why your first request can take 30–60 seconds (sometimes more). This behaviour is normal for free-tier cold starts — even though it feels excessive.

I’ll break it down clearly.


1. “Cold start” on free tier means: server is literally shut down

On Strapi Cloud Free Tier:

  • Your Strapi server is put to sleep when inactive

  • The first request must boot the entire Node.js runtime + Strapi app

  • This includes: loading plugins, schema, database connectors, middleware, routes, GraphQL/rest handlers, etc.

Strapi is not tiny — it’s a full CMS framework.

👉 Cold start of a full Strapi server can easily take 30–90 seconds on free-tier hardware
Node + Strapi initialization is slow, and on a lower-spec free VM, it’s even slower.

This is in line with other serverless platforms like Vercel / Netlify functions, which also have cold starts — but Strapi is heavier.


2. Strapi startup time includes preparing your database connection

Your database (SQLite/Postgres/others on Strapi Cloud) must also:

  • Spin up or reconnect

  • Warm database caches

  • Load content types

  • Sync permissions

  • Build Strapi’s internal content registry

This all happens BEFORE your request is resolved.

So:

✅ First request → boot server + boot DB + boot Strapi

✅ Subsequent requests → warm server → <1s

This exactly matches what you're seeing.


3. 1-minute cold starts have been reported by others

This is not unusual on free-tier Strapi Cloud.

Developers frequently report:

  • 45 seconds

  • 60 seconds

  • 90 seconds (occasionally)

Especially when the Strapi project:

  • Has many content types

  • Uses plugins

  • Has image processing enabled

  • Uses relations

Cold starts scale roughly with project size.


4. Why second load is always fast

After your first call:

  • Strapi app stays warm

  • Node runtime stays active

  • Caches warm

  • DB cached metadata stays warm

  • Plugin pipelines are ready

Hence your later calls take:

✅ 1–5 seconds (network latency + minimal processing)

Or faster in some cases.


...

6. Conclusion: Your 1-minute delay is NORMAL for Strapi free tier

Based on everything:

✅ Your behaviour is expected
✅ Not caused by your code
✅ Not caused by your React Query setup
✅ Not caused by your loading indicator or fetch method
✅ 100% due to Strapi Free Tier cold boots


7. If you ever want to improve this later

Minimal changes:

Option A — Upgrade Strapi Cloud to "Always On"

Cold start → 0 seconds
Requests → typically <300 ms

Even “Basic” tier solves this entirely.

Option B — Use a separate “health check” endpoint

Hit a lightweight endpoint every 5–10 minutes from a free uptime monitor:

  • UptimeRobot

  • Cron-job.org

This keeps the Strapi server warm.

⚠️ Some platforms disallow this and may still cold-start (but many developers report success).

Option C — Migrate backend

If you later outgrow the tutorial:

  • Supabase

  • Firebase

  • Appwrite

  • Payload CMS with Vercel edge

These have no 1-minute cold start.

But for now — your existing behaviour is fully normal.


...

Me: Very useful explanation. Thanks.

BTW Some NGOs get some free quota on AWS and Azure, I believe. Such an NGO can host the STRAPI server on their free quota AWS/Azure and get good first and later response time for STRAPI requests sent to that STRAPI server. Am I right?

[I have given this part of conversation in Details section.]

====================

23:01, 11 November 2025
eas whoami
npm run export-web
eas deploy

Above 3 commands run successfully; Last command's key output:
🎉 Your deployment is ready

Dashboard       https://expo.dev/projects/968daf71-15e9-4de5-975f-ace3c1c1859b/hosting/deployments
============

Confirmed from expo.dev that above deployment has been done.

Visited https://sg-eduapp-lms--excmsr0i6w.expo.app/ . It is working and shows the loading message that was added after previous deployment.
===================

19:43, 13 November 2025

I had mailed Simon Grimm two days back seeking permission to share my slightly modified version of his app code. Today he responded and provided permission.
... 

Modified loading message; Cleaned up code

Now going for production deployment of web app

lms> eas env:push --path .env
√ Select environment: » production
Uploaded env file to production.
lms> 

Confirmed from expo.dev that env variables have got set up for production 'Environment'.
So now the same env variables are set up for development as well as production.
-----

Regarding: *) After above, production deploy can be considered. With prod. deploy change below env var in .env.cloud.fs:
# EXPO_PUBLIC_WEB_SOON_URL=http://10.50.171.151:8081/soon
--- 
The above is related to waitlist option. But now Waitlist is not being used. Following line is commented in .env:
# EXPO_PUBLIC_CLERK_WAITLIST_ON=true

So I think EXPO_PUBLIC_WEB_SOON_URL should also remain commented and not updated to point to production deployed URL.
Am leaving it commented now.
----------------

Now I plan to run:
npx expo export --platform web
eas deploy --prod
----

lms> npx expo export --platform web
env: load .env
env: export EXPO_PUBLIC_STRAPI_API_URL EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY EXPO_PUBLIC_REVENUECAT_WEB_KEY STRAPI_API_ADMIN_TOKEN EXPO_UNSTABLE_DEPLOY_SERVER
Starting Metro Bundler
Static rendering is enabled. Learn more: https://docs.expo.dev/router/reference/static-rendering/
λ Bundled 5073ms node_modules\expo-router\node\render.js (1565 modules)
Web Bundled 5734ms node_modules\expo-router\entry.js (1543 modules)
λ Bundled 523ms app\api\add-user-course+api.ts (1 module)

› web bundles (2):
_expo/static/css/web-bb1b0ce2b5777e2abef11b66c70eb5eb.css (13 kB)
_expo/static/js/web/entry-256b86d3ea9c1e67463b424caaee573e.js (3.69 MB)

› Static routes (13):
/soon (16.8 kB)
/wait (16.8 kB)
/ (index) (16.8 kB)
/login (16.8 kB)
/_sitemap (16.8 kB)
/+not-found (16.8 kB)
/(app)/(authenticated)/courses (16.8 kB)
/(app)/(authenticated)/(tabs) (16.8 kB)
/(app)/(authenticated)/course/[slug] (16.8 kB)
/(app)/(authenticated)/(tabs)/profile (16.8 kB)
/(app)/(authenticated)/(tabs)/my-content (16.8 kB)
/(app)/(authenticated)/course/[slug]/[lesson] (16.8 kB)
/(app)/(authenticated)/course/[slug]/[lesson]/overview (16.8 kB)

› API routes (1):
/api/add-user-course (3.88 kB) (source map (5.84 kB))

Exported: dist
lms> 

lms> eas deploy --prod
EAS Hosting is still in preview and subject to changes.
> Project export: server - exported 1 minute ago
✔ Created deployment
|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■| 100% Uploading 43 assets
✔ Promoted deployment to production

🎉 Your deployment is ready

Dashboard       https://expo.dev/projects/968daf71-15e9-4de5-975f-ace3c1c1859b/hosting/deployments
Deployment URL  https://sg-eduapp-lms--dwky00hied.expo.app
Production URL  https://sg-eduapp-lms.expo.app
lms> 
===================

The app is deployed to: https://sg-eduapp-lms.expo.app/

git commit -m "Modified loading message; Cleaned up code; Created expo production deployment"

git commit -m "Improved Readme"

Made associated GH repo public: https://github.com/ravisiyer/SG-EduApp-LMS 
=======================

14:23, 14 November 2025

Generating a release APK

SG-EduApp> keytool -genkeypair -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000
Enter keystore password:

Re-enter new password:

What is your first and last name?
  [Unknown]:  Ravi Iyer
What is the name of your organizational unit?
  [Unknown]:  Individual
What is the name of your organization?
  [Unknown]:  Ravi Iyer Software Solutions
What is the name of your City or Locality?
  [Unknown]:  Puttaparthi
What is the name of your State or Province?
  [Unknown]:  Andhra Pradesh
What is the two-letter country code for this unit?
  [Unknown]:  IN
Is CN=Ravi Iyer, OU=Individual, O=Ravi Iyer Software Solutions, L=Puttaparthi, ST=Andhra Pradesh, C=IN correct?
  [no]:  yes

Generating 2,048 bit RSA key pair and self-signed certificate (SHA256withRSA) with a validity of 10,000 days
        for: CN=Ravi Iyer, OU=Individual, O=Ravi Iyer Software Solutions, L=Puttaparthi, ST=Andhra Pradesh, C=IN
[Storing my-release-key.keystore]
SG-EduApp>
------------

As my debug build ran well, I don't want to do a clean and build. CG is guiding me through release build without deleting android folder.

Copied my-release-key.keystore file to android/app/my-release-key.keystore

Edited android/gradle.properties and add (including changing password suitably):
MYAPP_UPLOAD_STORE_FILE=my-release-key.keystore
MYAPP_UPLOAD_KEY_ALIAS=my-key-alias
MYAPP_UPLOAD_STORE_PASSWORD=your_keystore_password
MYAPP_UPLOAD_KEY_PASSWORD=your_key_password
----
I used # character in my password. That seems to need escaping in gradle.properties.
to avoid issues, I am recreating the keystore file with simpler password 
Did it providing other details same as earlier.

----

6. Edited android/app/build.gradle
 In existing signingConfigs block which already had debug block, added release block:
signingConfigs {
debug { ... }
release {
storeFile file(MYAPP_UPLOAD_STORE_FILE)
storePassword MYAPP_UPLOAD_STORE_PASSWORD
keyAlias MYAPP_UPLOAD_KEY_ALIAS
keyPassword MYAPP_UPLOAD_KEY_PASSWORD
}
}
 In buildTypes release block deleted old signingConfig line and replaced it with: signingConfig signingConfigs.release
buildTypes {
release {
signingConfig signingConfigs.release
...
}
}
---------
CG: Correct — you do not need to add:
minifyEnabled false shrinkResources false
because your existing build.gradle is already handling these settings properly, using Expo/React Native defaults.
--------------

7. cd android

I am skipping '.\gradlew.bat clean' as that will delete debug APK file too. I don't think gradle clean is required now.

9. .\gradlew.bat assembleRelease

-------------

> Task :react-native-reanimated:buildCMakeRelWithDebInfo[armeabi-v7a] FAILED
C/C++: ninja: error: mkdir(src/main/cpp/reanimated/CMakeFiles/reanimated.dir/C_/Users/{username}/NIBSProj/SG-EduApp/lms/node_modules/react-native-reanimated/Common): No such file or directory

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':react-native-reanimated:buildCMakeRelWithDebInfo[armeabi-v7a]'.
> com.android.ide.common.process.ProcessException: ninja: Entering directory `C:\Users\{username}\NIBSProj\SG-EduApp\lms\node_modules\react-native-reanimated\android\.cxx\RelWithDebInfo\5t2h7556\armeabi-v7a'
  [0/2] Re-checking globbed directories...
........
[19/37] Building CXX object src/main/cpp/reanimated/CMakeFiles/reanimated.dir/094db2ac3ef658eb6557d2380c05f6ed/reanimated/Fabric/PropsRegistry.cpp.o
  ninja: build stopped: .

  C++ build system [build] failed while executing:
      @echo off
      "C:\\Users\\{username}\\AppData\\Local\\Android\\Sdk\\cmake\\3.22.1\\bin\\ninja.exe" ^
        -C ^
        "C:\\Users\\{username}\\NIBSProj\\SG-EduApp\\lms\\node_modules\\react-native-reanimated\\android\\.cxx\\RelWithDebInfo\\5t2h7556\\armeabi-v7a" ^
        reanimated ^
        worklets
    from C:\Users\{username}\NIBSProj\SG-EduApp\lms\node_modules\react-native-reanimated\android
  ninja: error: mkdir(src/main/cpp/reanimated/CMakeFiles/reanimated.dir/C_/Users/{username}/NIBSProj/SG-EduApp/lms/node_modules/react-native-reanimated/Common): No such file or directory

... 
BUILD FAILED in 14m 34s
726 actionable tasks: 694 executed, 32 up-to-date
android>

---------------

After rebuilds including after deleting caches (see details entry for procedure to delete caches), eventually I got to a non download error. That was:
> Task :react-native-reanimated:buildCMakeDebug[arm64-v8a] FAILED
C/C++: ninja: error: mkdir(src/main/cpp/reanimated/CMakeFiles/reanimated.dir/C_/Users/USERNAME/CurrProjects/VSCode/ReactNative/pizza-app/node_modules/react-native-reanimated): No such file or directory
Initially I thought it is a long path error as an earlier warning indicated a long path issue. So I modified Windows 11 registry to support longer pathnames. But that did not fix above error. The eventual solution was from https://stackoverflow.com/questions/79103323/cmake-build-error-with-react-native-reanimated-mkdir-no-such-file-or-director . I did the following:
Installed CMake latest version of 3.31.6
$Env:CMAKE_VERSION = '3.31.6'
npm run android
That fixed the issue and the development build was successful. It created a development/debug APK file.
==== end extract from blog post ==============

Trying:
$Env:CMAKE_VERSION = '3.31.6'
followed by
.\gradlew.bat assembleRelease
---

It has crossed the above error point ...
...
BUILD SUCCESSFUL in 24m 53s
1454 actionable tasks: 772 executed, 682 up-to-date
android>
============

CG: Yes — this exact error is a known issue with React Native Reanimated + Android NDK + CMake on Windows.

Your fix ($Env:CMAKE_VERSION = '3.31.6') is the correct workaround.
Here is what was actually happening:


Root Cause: Path-length + CMake version bug on Windows

The critical line in your error:

mkdir(...C_/Users/{username}/NIBSProj/SG-EduApp/lms/node_modules/react-native-reanimated/Common): No such file or directory

This happens due to two combined problems:

1. Windows file path too long

Reanimated generates extremely deep native build paths:

...react-native-reanimated/android/.cxx/RelWithDebInfo/5t2h7556/armeabi-v7a/...

Windows has a path limit of 260 characters unless registry keys or group policies remove the limit.

Ninja + CMake sometimes fail to create folders when the path hits this limit.

2. Bug in older CMake versions (like 3.22.x bundled with Android Studio)

The version installed inside:

AndroidSdk/cmake/3.22.1

has known Windows bugs:

  • incorrect handling of long paths

  • incorrect normalization of C:/C_ in ninja rules

  • failure to create intermediate directories

This is exactly what your log shows:

CMakeFiles/reanimated.dir/C_/Users/...

That C_ is a known CMake path normalization bug.


Why your fix worked

You set:

$Env:CMAKE_VERSION = '3.31.6'

This forces Gradle to use your newer CMake version, not the buggy 3.22.1.

Newer CMake versions (3.25+) fix:

  • long path handling

  • Windows absolute path normalization

  • Ninja directory creation bugs

So the reanimated native build succeeded on the next attempt.


📌 This matches a known issue

Multiple developers reported identical errors when building Reanimated on Windows:

✔ fails with CMake 3.18–3.22
✔ succeeds with CMake 3.25–3.31
✔ issues appear only during release builds (because CMake builds more artifacts)


Recommended Long-term Fix

To avoid this in future:

Option A — Add this to gradle.properties

android.cmake.version=3.31.6

(Works on recent AGP versions.)

...
Me: IFIRC, I have already set Windows registry key for long paths

CG: Even with LongPathsEnabled = 1, Windows will allow long paths, but older CMake + Ninja versions still fail because they simply do not support long paths internally.

So you can have:

✔ Windows OS long-path support enabled

✘ CMake 3.22 / Ninja still using legacy path-handling

→ build fails

This is why the issue still occurs even after enabling the registry key.


Why Long Paths Enabled Alone Doesn’t Fix It

1. CMake 3.22 internally truncates or rewrites paths

Example from your log:

C_/Users/...

This is CMake’s own faulty path normalization—not Windows’ fault.

2. Ninja fails on certain deep folder creations

Old Ninja versions (bundled with CMake 3.22) still choke on deep paths even when Windows supports them.

3. Reanimated + New Architecture + CMake produce extremely deep paths

The .cxx/ build folder tree is very long, easily exceeding 260 chars.

So the registry fix helps OS-wide, but CMake 3.22 is still buggy.


Why switching to CMake 3.31.6 fixes everything

CMake 3.25+ includes:

  • ✔ correct Windows long path APIs

  • ✔ correct conversion for C:/ paths

  • ✔ fixed Ninja rule generation

  • ✔ improved directory creation logic

That’s why your build succeeded immediately after setting:

$Env:CMAKE_VERSION = '3.31.6'

Summary

Even though you had Windows long paths enabled, the real problem was:

👉 CMake 3.22’s Windows path bugs

Newer CMake versions fix those bugs, so your release APK built successfully.

=======================

The release APK is 123 MB.
Was able to rapidly (less than half a minute, I think), send it to mobile via QuickShare. I had already uninstalled debug app on mobile. Installed the release and ran it. Logged in. After login, it asked which app should be used to open (something) - I used to see this with debug app but thought that I would not be seeing with release. Anyway, chose lms and that worked correctly. The app functionality was also OK. 
Logged out and logged in again. This time I was not asked to choose an app for opening (something) and was straight away shown the home screen in the app.
============
I uninstalled the release app and reinstalled it. Once again, after login I was asked:
Open with (and provided the following 3 options):
CrudApp
lms
StickerSmash
----------

CrudApp and StickerSmash are old learning apps that I had created in the past.

CG: Got it — and this confirms the root cause with perfect clarity:

✅ Why you are seeing the “Open with” dialog

Because these old apps — CrudApp and StickerSmash — still have intent filters in their manifests that match the same deep link URL scheme used by your LMS app.

So Android sees:

“Three apps can open this URL. Ask the user which one to use.”

...
Me: The app.json for all 3 apps (which are React native and Expo apps) has scheme as "myapp"

CG: Ah — that explains everything perfectly.

All three apps (LMS, CrudApp, StickerSmash) have:

"scheme": "myapp"

So Android sees all three apps as valid handlers for any deep-link like:

myapp://something

Therefore, after your Google SSO login finishes and redirects to:

myapp://redirect/clerk

Android correctly shows this dialog:

Open with: ✔ CrudApp ✔ lms ✔ StickerSmash

Because all three apps advertise themselves as handlers for the same URL scheme.


Fix: Give each app a unique scheme

For LMS, choose something stable and unique:

Recommended:

"scheme": "lms"

or slightly more unique:

"scheme": "lmsapp" "scheme": "lms-ravisiyer" "scheme": "lms-ravi"

Do NOT use myapp for production apps.
It is too generic and will conflict on your phone and on users' phones.

...
Me: Understood the issue - Great. Thanks a ton.
I could use a scheme name of "sg-ravisiyer-lms". That would be quite unique.
But will that need a complete rebuild of the web and Android app?

CG: Yes — changing the scheme requires a full rebuild of your mobile apps, but NOT your web app.

Here is the impact:


Android (and iOS)Rebuild is required

Deep link schemes are baked into the native code at build time.

So after changing:

"scheme": "sg-ravisiyer-lms"

you must rebuild:

  • Release APK/AAB (Android)

  • iOS build (if you ever create one)

Otherwise, Android won’t know that your new scheme exists.


Web appNO rebuild needed (unless redirect URLs change)

The web app does not use the mobile scheme.

Only if you're hosting web on expo.dev classic, the redirect URL used by Clerk might need updating, but usually it’s unrelated.

Web deep linking and mobile deep linking are separate.

...
[I shared login.tsx code.]

CG: Great — this confirms something important:

Your code does not specify a redirect URL manually.

When using:

const { startSSOFlow } = useSSO();

Clerk Expo automatically builds the redirect URL from the Expo app scheme.

That means:

Clerk builds redirect URL as:

<your-app-scheme>://oauth-native-callback

So if your app.json is currently:

"scheme": "myapp"

then your SSO redirect URL is:

myapp://oauth-native-callback

🔥 Why this matters

Since all three apps (CrudApp, LMS, StickerSmash) use:

"scheme": "myapp"

they all have the same redirect URI registered on the device.

So when Google finishes login and tries to open:

myapp://oauth-native-callback

Android says:

“I found three apps that can handle this. Which one do you want?”

Hence the dialog.


🧩 What you need to do

Change your LMS scheme in app.json to something unique:

"scheme": "sg-ravisiyer-lms"

Then Clerk will automatically use:

sg-ravisiyer-lms://oauth-native-callback
...
Me: The tutorial author has used lms in his app.json.
But if some other lms app also uses Clerk and uses SSO login the same way, and happens to use same scheme as lms (which is not so improbable), and if both these apps are on same Android phone, then this open with issue will crop up. am i right? 

CG: Yes — you are 100% right.

This is not just theory — it is exactly how Android behaves.


If two apps use the same scheme (lms://) and both are installed on one phone:

  • Both apps tell Android:
    “I can handle URLs that start with lms://.”

  • Clerk OAuth returns the SSO redirect to:

    lms://oauth-native-callback
  • Android performs a lookup → finds two matching apps.

  • Android must ask the user:

“Open with?”
App A
App B

Exactly the same situation you faced.

...
Me: I am sure Clerk would provide a way to specify a more unique redirect url than just the scheme. It may just be that the simplest use of Clerk uses scheme for the redirect URL. what do u say?

CG: You are completely correct — Clerk does allow more specific and more unique redirect URLs.

The reason you only see a simple scheme://redirect (or scheme://oauth-native-callback) is because that’s the default, simplified path provided for Expo developers.

But under the hood, Clerk fully supports a more granular, unique redirect URL.

[CG gave some details of how Clerk provides such support which I am omitting.]

Me: Ok. I will change the scheme in app.json to sg-ravisiyer-lms. Is that a valid scheme name?

CG: Yes — sg-ravisiyer-lms is a valid scheme name.

...
Me: My app.json expo block currently:
{
  "expo": {
    "name": "lms",
    "slug": "sg-eduapp-lms",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/images/icon.png",
    "scheme": "myapp",
    "userInterfaceStyle": "automatic",
    "newArchEnabled": true,
    "ios": {
      "supportsTablet": true,
      "userInterfaceStyle": "automatic"
    },
---------------

Hmm. so looks like I did specify lms as the name of the app when I ran create-expo-app.
So looks like create-expo-app always uses myapp for the scheme name. am i right?

CG: Yes — you are right.

create-expo-app always defaults the scheme to "myapp"

===========

Made the change in app.json:
    "scheme": "sg-ravisiyer-lms",
----

[I shared app.json with CG.]

CG: Great — I checked your entire app.json very carefully.

Here is the definitive conclusion.


1. You have no deep-linking config except the single scheme field

============

1. Deleted android folder
2. npx expo prebuild --clean --platform android
Skipping npx expo-doctor
4. Copied my-release-key.keystore file to android/app/my-release-key.keystore
5. Edit android/gradle.properties as noted above.
6. Edited android/app/build.gradle as noted above.

7. cd android
8. .\gradlew.bat clean
BUILD SUCCESSFUL in 1m 13s
63 actionable tasks: 48 executed, 15 up-to-date

9. $Env:CMAKE_VERSION = '3.31.6'
.\gradlew.bat assembleRelease

BUILD SUCCESSFUL in 23m 10s
1454 actionable tasks: 1440 executed, 14 up-to-date
android> 

========================

Uninstalled old app, installed new app on mobile. On opening it, now I did not get the 'open with' screen.
I did not make any changes to Clerk dashboard.

[CG had earlier said that Allowlist in Clerk dashboard will have to be modified.]

CG: Clerk’s free tier does NOT require you to manually add the redirect URL to an allowlist for mobile apps.

Why?

Because for mobile Expo/React Native apps Clerk automatically allows custom-scheme redirects ...

...
Me: I see. My 'production' web URL on Expo is https://sg-eduapp-lms.expo.app/
U r saying that that is auto-trusted by Clerk and so I did not have to add to allowlist.
did i get that correctly?

CG: Yes — exactly correct.
------------------


Me: I want to now install and test the apk on android emulator. My android emulator setup frequently complains of not enough space to install an apk. 
...
[I moved this part to Details section.]

[Sometime earlier I had downloaded Google Drive APK file using shared link on Android emulator.]

Me: ... I installed my app on the emulator. ...
...

Me: On the emulator too, the app did not ask for 'open with' options. Great.
Now as I changed app.json (scheme name change), perhaps I should create a web dist again and deploy it to expo. Then I can test the web app on expo and confirm it is working properly after scheme name change. What do u say?

CG: Yes — that’s a good idea.

Here’s why:

  • The scheme change in app.json affects deep linking, and while it primarily impacts mobile apps, it can also affect any web links or redirects that rely on the app’s URL scheme, especially if you’re using expo-router or similar routing.

  • Creating a fresh web distribution ensures that:

    1. All deep link handling and routing is consistent with the new scheme.

    2. Any potential cached URLs or old builds don’t interfere.

    3. You can confirm that the Clerk login / SSO flow still works correctly in the web version.

=================

16:44, 15 November 2025

Uploaded the APK to Virus Total.


The scan reported the APK file to be clean i.e. "No security vendors flagged this file as malicious".
===================

20:00, 15 November 2025

git commit -m "Made scheme virtually unique (sg-ravisiyer-lms) in app.json; Rebuilt release APK"

Ran:
npx expo export --platform web
eas deploy --prod
----
Both were successful.
Same Production URL  https://sg-eduapp-lms.expo.app

The deployed web app worked OK. Logged out and logged in again on it. That worked OK. So new scheme has not tripped up Clerk

git commit -m "Updated Readme with APK link and more"

Saw that a user got added to Strapi cloud backend some hours back today. 
Also saw that the same username, email combo got added twice! the ClerkId for both user entries are the same!
But Clerk shows only one user with that username with same ClerkId as that in Strapi.
So looks like there is a bug in the app that results in two user entries getting created in Strapi under some conditions.
But eklavyasai user is not listed in Strapi. However, he is associated with a course in Strapi and also in app UI. I could progress in that course and a progress record got added to Strapi backend.
So is eklavyasai user record being wrongly shown as the newly added user in Strapi admin panel? I think that probability is very low. I may have manually deleted eklavyasai user from Strapi cloud.

Me: In a React native and expo frontend project, I have a Strapi backend (hosted now on Strapi cloud). I was surpised to see a duplicate entry for a user in User collection. The frontend uses POST with /api/auth/local/register to add a user.
In some condition, I guess the frontend invokes it twice (some race condition perhaps).
But does not Strapi backend reject the second duplicate entry for user?

[CG discussion led to me trying to check whether username and email are marked as unique in local Strapi server which was uploaded to Strapi Cloud. Strapi Cloud operates in some production mode where it may not be showing some details.]

Me: Hmm. On my local strapi server, I am not able to login. It is saving invalid credentials. I think I am specifying the correct values. I tried the forgot password link but that is resulting in failure to send mail on the local server.
...
Me: Ravi-lms-api> npx strapi admin:create-user
? Admin email? riyer02@gmail.com
? Admin password? [hidden]
? First name? Ravi
? Last name? Iyer
? Do you really want to create a new admin? Yes
User with email "riyer02@gmail.com" already exists
Ravi-lms-api>
...

Me: Ravi-lms-api> npx strapi admin:reset-password
? User email? riyer02@gmail.com
? New password? [hidden]
? Do you really want to reset this user's password? Yes
Successfully reset user's password
Ravi-lms-api>

Can I check login/pwd using a command like the above

CG: ... Strapi does NOT provide any CLI command to “test” or “verify” an admin login.** 
...

Me: Now I could login as admin on local server (using browser). Thanks a ton.
...

Me: I think it is a Strapi bug - See Duplicated account created in Registration when concurrently called to Strapi, https://github.com/strapi/strapi/issues/17504

In my case, I think a user tried twice to login while Strapi cloud was doing cold start. That may have resulted in two very close to each other /concurrent register requests to Strapi which when it finished cold start resulted in immediate addtion of two users with same data.

CG: Yes — your deduction is correct, and that GitHub issue you found matches the behavior perfectly.
...

Me: There is no option to set unique for username and email. A lock sign is shown next to them indicating they cannot be edited. I think this is built-in user feature being used.

CG: Ah — this new detail changes the interpretation completely.

The lock icon tells us exactly what is going on.

✅ If you see a lock icon next to email and username,

then you are NOT looking at a custom User content type.

You are actually looking at:

Strapi's built-in Auth model (Users-Permissions plugin)

even though it appears inside Collection Types.

Strapi v4 places the Users table (“Users-Permissions Users”) under Collection Types — but shows a lock icon because it is not editable.

...
Me: Will there not be a unique constraint on underlying database for the field like email? So even if Strapi code fails, will not the database (I don't know which database is being used - let's presume PostgreSQL) prevent the duplicate email field (as it is unique).

[CG said that underlying database does not have unique constraint.]

Me: How do you know that Strapi Cloud does not create a database-level unique constraint on the email field of the Users-Permissions User table

CG: ... Short answer: I don't have any public documentation from Strapi Cloud saying “we don’t create a DB-level unique index for email” — so strictly speaking, it's an inference based on community bug-reports, issue behavior, and your observed symptoms.
...

Me: Hmm. So unless one is able to directly view the underlying database Strapi cloud is using, we cannot be sure.
...

Me: Can I check the underlying database for local Strapi?
...

[CG gave some directions.]

Me: database.ts has:
  const client = env('DATABASE_CLIENT', 'sqlite');

CG: Great — since your local Strapi is using SQLite, it's very easy to inspect the actual database and confirm whether the email field in the Users table has a UNIQUE constraint.
...

[IFIRC, based on CG input I ran: 
sqlite3 .tmp/data.db
which gave the sqlite3> prompt for executing commands.]

Me: sqlite> .schema up_users
CREATE TABLE IF NOT EXISTS "up_users" (`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `document_id` varchar(255) NULL, `username` varchar(255) NULL, `email` varchar(255) NULL, `provider` varchar(255) NULL, `password` varchar(255) NULL, `reset_password_token` varchar(255) NULL, `confirmation_token` varchar(255) NULL, `confirmed` boolean NULL, `blocked` boolean NULL, `clerk_id` varchar(255) NULL, `created_at` datetime NULL, `updated_at` datetime NULL, `published_at` datetime NULL, `created_by_id` integer NULL, `updated_by_id` integer NULL, `locale` varchar(255) NULL, CONSTRAINT `up_users_created_by_id_fk` FOREIGN KEY (`created_by_id`) REFERENCES `admin_users` (`id`) ON DELETE SET NULL, CONSTRAINT `up_users_updated_by_id_fk` FOREIGN KEY (`updated_by_id`) REFERENCES `admin_users` (`id`) ON DELETE SET NULL, CONSTRAINT `up_users_created_by_id_fk` FOREIGN KEY (`created_by_id`) REFERENCES `admin_users` (`id`) ON DELETE SET NULL, CONSTRAINT `up_users_updated_by_id_fk` FOREIGN KEY (`updated_by_id`) REFERENCES `admin_users` (`id`) ON DELETE SET NULL);
CREATE INDEX `up_users_documents_idx` on `up_users` (`document_id`, `locale`, `published_at`);
CREATE INDEX `up_users_created_by_id_fk` on `up_users` (`created_by_id`);
CREATE INDEX `up_users_updated_by_id_fk` on `up_users` (`updated_by_id`);
sqlite>

CG: ...

Your SQLite schema shows that Strapi did NOT create a unique constraint on email or username.

Here is what we expect if uniqueness were enforced:

UNIQUE ("email")

or

CONSTRAINT up_users_email_unique UNIQUE (email)

But your actual schema is:

`email` varchar(255) NULL,

with no UNIQUE constraint.

That means:

  • The database allows multiple users with the same email.

  • Strapi's application-level validation is the only protection.

  • Therefore, any race condition, cold start, or concurrency bug can create duplicates.

  • This is true locally and in Strapi Cloud (even more so because Cloud does not expose the schema).


🚨 Conclusion

Strapi’s Users-Permissions up_users table does not enforce unique email or username at the database level.

This matches what you observed in Strapi Cloud:

  • Two identical users created during cold start.

  • No DB constraint to reject the second insert.

  • Known Strapi bug allowing duplicate user creation.

So the behavior you saw is not surprising — the DB layer simply does not protect you.

...
Me: The app uses Clerk and Google SSO for auth. It associates ClerkId with each created user in Strapi. Further in the app's data functions it uses Clerk.Id for filter. See code below:
  const getUserCourses = async (): Promise<UserCourses[]> => {
    try {
      const url = `${baseUrl}/api/user-courses?filters[clerkId]=${user?.id}&populate[course][populate]=image`;
      const response = await fetch(encodeURI(url));

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const result = await response.json();
      result.data.forEach((entry: any) => {
        entry.course.image = (__DEV__) && backendMode === 'local'
        ? `${baseUrl}${entry.course.image.url}`  // In Development
        : `${entry.course.image.url}`;
      });
      return result.data;
    } catch (error) {
      throw error;
    }
  };
================

The user object in code above comes from Clerk.
So I think duplicate user entries in Strapi may not create a problem for the app.

CG: You're absolutely right — given your architecture (Clerk + Google SSO + Strapi), duplicate Strapi users will not break your app’s functionality, and here’s why.
...

Me: the two user entries are completely identical including ClerkId.
but as the key User-Course and Progress collection types which seem to be the only collections updated based on user activity, both have ClerkId field in them and not Username/email, and the StrapiProvider code in app frontend filters by ClerkId, I think duplicate user entries will not matter for app functionality.

CG: Yes — your reasoning is correct.
If every part of your app logic keys off ClerkId, and all user-specific relations in Strapi also use ClerkId, then duplicate Strapi “User” entries with the same ClerkId will not break app functionality.
...

Me: ... The GitHub issue I shared with you earlier seems to have got closed as the person looking at it could not reproduce the problem! 

==============
As testing afresh, I deleted:
My two Strapi users but not the remaining two entries of some other user
All Strapi user-course entries (all related to my userids)
My three Clerk users (userids). I did not delete the other user.

So now I think I have a clean slate for testing Android app and web app with my userids: Signing in as new user, enrolling in course etc.

I have to test addition of users from Android app and expo web deployment app.

After testing, I have to add appreciation comment to SG YT. Followed by mail to him about short addition to it about this modified version, if he is OK with it.
===============

21:01, 16 November 2025

The production app on android on my phone does not show the tabs at the bottom typically, though I think I saw it once or twice.
Check if on android emulator, whether the production app has the same issue.
-----------

I think there is a bug in the Android app as 'Start Course' button is not doing anything. I had not tested this in the production app as I had already started the courses and the production app straight away took me to the lessons screen. Today I am testing after deleting my user entries and signing up afresh. That's when I am seeing the problem.

The production web app - https://sg-eduapp-lms.expo.app/ - does not have this problem. 'Start Course' in it, started the course and took me to lessons screen.

I have to debug the android production app issue.
....

I am seeing a bug in the production Android app. to debug it, I need to create a debug APK.
So I plan to do the following:
cd android
.\gradlew.bat assembleDebug

BUILD SUCCESSFUL in 20m 16s
1101 actionable tasks: 1037 executed, 64 up-to-date
android> 

In the development build:
'Start Course' button works and I can see lessons in the course.
Bottom tabs are shown.

=======================

[CG helped me get the issue. But it took lot of exchanges as various other possiblities were ruled out before I shared the addUserToCourse() function from StrapiProvider with CG. Then the api issue came to the surface.]

CG: Here is the real root cause with 99% likelihood, based on your symptoms:

Your mobile production APK is calling a relative URL:

fetch(`/api/add-user-course`, { ... })

On Android production build, this becomes:

https://your-app-domain.com/api/add-user-course ❌ (invalid)

But Android does NOT know your Expo dev server domain, so:

→ The POST request never reaches your Next.js API

→ Therefore it never reaches Strapi

→ Strapi logs show no PUT or POST

→ Exactly what you observed

→ Debug APK works because it runs in Metro dev environment

→ Web works because browser resolves relative URLs correctly

...

[I shared the app\api\add-user-course+api.ts code.]

CG: Great — now your architecture becomes very clear, and this confirms the diagnosis:

Your production Android APK is trying to call:

/api/add-user-course

which is a Next.js App Router API Route inside your Expo web build, not Strapi.

...

Me: I think you have got the issue. Some info.

1) I am following a tutorial. The author demonstrated it on iOS and web. I think he demonstrated a dev build on iOS. So he did not face the problem. He may never have created a production build and tried it out.

2) I think the tutorial is meant to use the API from deployed web server. So I would like to direct the API call to the deployed web server which is: https://sg-eduapp-lms.expo.app/

...

Me: https://sg-eduapp-lms.expo.app/api/add-user-course in browser gives:
No matching method handler exists for this path
 A network connection was established successfully.
 Method Not Allowed
The worker does not define a handler for GET methods and the request has been rejected.
Check that you're sending a request for the correct method.
Request ID: 99f8f12b9b5a9be5

CG: Great — this is exactly the correct response and it confirms that:

✅ The API route exists

✅ It is deployed correctly on the Expo web server

✅ It only accepts POST, and not GET, which is expected

So your mobile app can call:

https://sg-eduapp-lms.expo.app/api/add-user-course

with a POST request, and it will work.

...

Me: I think the fetch URL should be changed only for mobile. so we can have a platform.OS check

...

Me: You have to also check that it is production

...

Me: HOw's this code:
      let API_BASE = ''; // relative for web + all dev builds; Below override for native production builds
      if (!(__DEV__) && Platform.OS !== 'web'){
        if (!process.env.EXPO_PUBLIC_WEB_API_URL) {
          throw new Error("EXPO_PUBLIC_WEB_API_URL is not defined. It has to be defined for Native production build");
        }
        API_BASE = process.env.EXPO_PUBLIC_WEB_API_URL;
      }

CG: Your new version is excellent — in fact, it is the safest and clearest version so far.

...

Me: But even the thrown error will be silently swallowed in production APK. Am I right?

...

Me: Now a general point for my learning. ... I am new to mobile app dev.
In other environments like web or desktop/server programs, production app/program will log info. about critical failures. these logs are very helpful to identify why some functionality did not work. Even in this app, the Strapi server has logs. It is the native app where for production we have nothing to go with (users will not do adb stuff).
What is the industry norm for capturing critical error info. in native apps.

CG: ...

Below is a clear and practical summary for your learning.


✅ Industry Standards for Native App Error Logging

1. Crash + Error Reporting Services (the real backbone)

These services run inside your app and automatically capture:

  • Crashes

  • Uncaught exceptions

  • Network failures

  • API errors

  • Custom “critical failure” events you log manually

  • Device info (OS version, model)

  • Breadcrumbs (actions leading up to the crash)

🔥 The industry-default tools

A) Sentry (most popular, widely used in React Native)

  • Tracks crashes + handled errors

  • Shows stack traces

  • Lets you send custom events

  • Works with Expo

  • Lightweight and reliable

👉 Almost every professional React Native app uses Sentry.

B) Firebase Crashlytics (Google)

  • Very common in Android ecosystem

  • Automatically captures crashes

  • Lightweight

  • But less flexible than Sentry for logging custom errors

C) Expo Application Services Monitoring (new)

  • Expo is rolling out its own monitoring service

  • Not as advanced yet as Sentry

...
Me: Got it. Makes sense. Thanks for the explanation. In future, I will try out Sentry as it has a free tier (for solo developer which is what I am).

Now about another issue I am seeing in production build but not in debug build for Android app ...
The production app on android on my Samsung M21 phone (where setting is to always show system buttons at bottom) does not show the tabs at the bottom typically, though I think I saw it once or twice.

But production app on android emulator (which hides system buttons at bottom) shows the tabs.

CG: This behaviour is very common on Android production builds and the root cause is almost always the same:

⭐ Your bottom tabs are being hidden by the Android system navigation bar.

And the difference you observed between:

  • Samsung M21 (software buttons always visible) → tabs hidden

  • Emulator (gesture navigation, buttons hidden) → tabs visible

…is exactly the expected behaviour if your app UI does not account for the “navigation bar height” inset.

Let me explain cleanly.


✅ Why debug build works but production build fails

In debug builds, React Native + Expo often runs inside a debugging shell that automatically adjusts for insets.

In production builds, the real OS window insets apply, and if your layout is not using Safe Area / Insets properly, the bottom of the screen can get overlapped by:

  • Samsung 3-button nav bar

  • Gesture handle bar (on newer devices)

  • OEM-specific extra padding

So the bottom tab bar gets pushed under the navigation bar → becomes hidden.

This is one of the most common Android UI issues in React Native.


⭐ THE REAL ROOT CAUSE

Your app is not using SafeAreaView / safe-area-context for bottom padding around the tab bar.

Expo Router and React Navigation rely on safe area insets to avoid overlap, but this only works if the screen layout includes it.

...

Me: Once again tutorial author was on iOS which probably auto hides bottom navigation bar. Further he did not do a production build.

CG: ...

iOS always manages safe areas automatically and consistently.
The bottom home indicator area is well-defined, and React Navigation handles it smoothly.

Result: tab bar never overlaps.

[Then we had some exchanges about the solution. It was long-drawn as I had to test with a release build every time. Finally I got the solution with a mix of input from CG and from web search.]

Me: ... IFIRC, once or twice, the production app did show bottom tabs on my Samsung M21. Most of the times when I ran the app it would not show. But for one or two such runs it showed.

CG: Yes — what you observed can absolutely happen, and it fits exactly with how Android + production builds + Expo Router behave.

This “sometimes visible, sometimes missing” tab bar is a known pattern caused by race conditions + environment differences in production apps.

[I am omitting the detailed explanation as I am not sure about its accuracy.]

[I also looked at some other issues while I was trying out fix for the bottom tabs in production.]

============================
10:52, 17 November 2025

I think the app has a flaky kind of bug. In debug APK on phone, I signed out as dG user and signed in as eklavya. Eklavya is a new user at this stage. But after sign in, I was shown one course in My Courses tab ! It should have been 0 courses. I checked Strapi cloud backend. Its data did not shown a user-course entry for eklavya.
I closed the app and reopened it. Now Eklavya was auto signed in but this time, My Courses was showing 0 courses. So looks like there is bug in app related to reset of app data when user signs out which manifests if user then signs in with another user's crendentials without closing the app.
--------

Another bug: As Eklavya, clicking Start Course was not giving any feedback for few seconds. So I clicked it multiple times. That resulted in two entries in user-course for same Beginner's course, and My Courses showing two duplicate Beginner's course entries!

[Added fix to make Start Course disabled when it was processing earlier click/tap.]

Me: Made the changes.
About this code in main body of component:
  if (!course) {
    return (
      <View className="flex-1 items-center justify-center mt-10">
        <Text>Course not found</Text>
      </View>
    );
  } else {
    // Check if user has course access already
    userHasCourse(course.documentId.toString()).then((result) => {
      setHasCourse(result);
    });
  }
---------

While it will execute on every render the 'setHasCourse(result)' will trigger a re-render only when result value is different from what the hasCourse boolean state variable currently has. Is my understanding correct?

CG: ... setHasCourse(result) will still trigger a re-render even if result is the same boolean value as the current hasCourse state.

React does not skip re-render for primitive values like booleans when you call setState with the same value.

React simply enqueues a state update → component re-renders → React compares old vs new DOM and decides what to update.

...

✔️ Final Answer

No — React will re-render even if the state value does not change.
So your code will cause repeated re-renders and repeated async calls.

Use useEffect instead to avoid this problem.

[CG suggested some useEffect code.]

Me: Why have cancelled variable in the useEffect?

CG: The cancelled variable is used to prevent updating state after the component has unmounted or after the useEffect has been restarted.

This is a standard React pattern for avoiding async memory leaks.

...

When you call an async operation inside useEffect:

userHasCourse(...).then(result => {
  setHasCourse(result);
});

The promise may resolve after:

  • the component has unmounted, or

  • the effect dependency has changed (so the previous effect should no longer update state)

If the old async call resolves later and you call setHasCourse, React logs a warning:

Can't perform a React state update on an unmounted component.

This can happen in:

  • fast navigation

  • slow network

  • user switching courses

  • component unmount/remount cycles


✅ What the cancelled flag does

Inside the effect:

let cancelled = false;

userHasCourse(...).then(result => {
  if (!cancelled) {
    setHasCourse(result);
  }
});

return () => {
  cancelled = true;
};

This works like this:

  • Effect runscancelled = false

  • If the component unmounts OR effect dependencies change → cleanup runs → cancelled = true

  • Now, when the async function resolves, it checks cancelled

  • Since cancelled === true, it skips the setState

This prevents React from updating an unmounted component.

...

Me: Nice touch!

...

Me: trying out the code  now in Debug APK which is using Strapi cloud.

The 'Start Course' button seems to be less responsive. I have to tap on it a few times after which it shows the activity indicator and does the processing correctly. Why do I have to tap it a few times?

CG: ... When you have ScrollView + nested Pressable, Android often “steals” the first tap for scroll activation unless:

  • The touch lands perfectly on the “Pressable”

  • The ScrollView is not interpreting the touch as a scroll start

So the first tap is frequently eaten by ScrollView, and only the second or third registers.

This is very common on Android.

...

Me: I will try out the above. But before that, does iOS have this issue? Probably not, as otherwise the tutorial author would have noticed it.

CG: You're exactly right — iOS does NOT have this issue, or has it very rarely, which is why:...

[Some trials ...]

Me: ...
    <Animated.ScrollView
      scrollEnabled={true}
      keyboardShouldPersistTaps="handled"
----------
worked.
On first tap, the Activity Indicator is shown and the user gets added to course.
I also tried rapid taps - that did not create multiple user-course entries.

...

git commit -m "Android production: Start course and bottom tabs fixes; Duplicate 'Start Course' tap/click handled"

git commit -m "On web, after purchase course, directly taking user to course overview page"

Me: I am ready to test the production Android APK (for the api part)
...Will now run:
.\gradlew.bat assembleRelease

...
Me: Installed release APK on phone. It shows the tab bar at the bottom
Using Eklavya. single tap on 'Start Course' for Beginner's course, started activity indicator and then showed Course Card with 0% against it. Clicking on Course Card led to Course Overview page. Start Course button was shown in bottom (floating). Clicking it started the course.
The Strapi cloud log had:
[2025-11-17 10:57:44] [2025-11-17 10:57:44.544] http: PUT /api/user-courses/usyf1rr12qeit15uw9g8sanw (236 ms) 200
Also its user-course collection had a new entry for this action.

The Expo dev deployment Requests entry had:
Requests > /api/add-user-course

ROUTE 200
Type  Success

Request details
Status 200 POST
Route /api/add-user-course
Path /api/add-user-course
Time Nov 17 16:24:45
Deployment sg-eduapp-lms--rmc7s0vsse
Operating system Android
Continent Unknown
Country 🇮🇳 IN
Region TG
Custom domain 
Duration 2,026ms
Request ID 99feb120f97b7aff
-----------------

The time difference in Strapi and Expo Dev seem to be related to timezone they are using. The above entries are among the latest entries in Strapi and in Expo.Dev

So the API part of going to Expo web server seems to be working OK.

CG: Excellent! 🎉

That confirms everything we aimed for with the production Android APK:

  • Tabs: Showing correctly at the bottom on your Samsung M21.

  • Start Course / Activity Indicator: Immediate feedback on first tap.

  • Single Entry: Only one user-course entry created even if tapped multiple times.

  • Navigation: Works correctly from Course Card → Overview → Start Course.

  • Strapi Logs: Shows PUT /api/user-courses/... with 200, confirming backend update.

  • Expo Dev Deployment Logs: POST to /api/add-user-course succeeded.

The slight time difference between Strapi and Expo logs is just a timezone/display thing — nothing to worry about.

Your production build is now behaving exactly like the debug build in terms of API functionality. ✅

...

[Now trying out premium course purchase for Eklayva user on Chrome mobile while I am logged in on Android app too on same phone/device.]

--------
Me: Strange! After I closed the android app and reopened it, it is showing only top edge of tab bar. Rest is obscured by navigation bar.

============

git commit -m "Trying to fix bottom tab visibility on Android; Modified start course to go directly to overview without toast/alert"

...
================


[Lots of trials ... Eventually I got the fix with SafeAreaProvider and SafeAreaView in the top level layout file but omitting top edge ...]

Me: In debug APK, I solved the gap at the top by modifying the SafeAreaView as follows:
                  <SafeAreaView style={{ flex: 1 }} edges={['bottom', 'left', 'right']}>
                    <InitialLayout />
                  </SafeAreaView>
-----
I dropped 'top' from edges as given in some example(s) here: https://github.com/AppAndFlow/react-native-safe-area-context/issues/167

...

To prevent conflict, I added SafeAreaView only in the top app\_layout.tsx . I commented it out in tabs layout.

Me: I think dropping the top may be the helpful thing for release APK too, as IFIRC a vertical gap had appeared at the top and that may have pushed the bottom tabs down due to which only their top edges were visible. What do u say?

...

Me: We are using both SafeAreaProvider and SafeAreaView in our top level layout. Please explain role of each.

CG: ... 

1. SafeAreaProvider

  • Purpose: It’s the context provider for the safe area information.

  • What it does:

    • Measures the device’s safe area insets (top, bottom, left, right), which vary depending on:

      • Notches (iPhone X+)

      • Status bar height

      • Navigation bar height (Android)

      • Rounded corners

    • Makes these insets available to all SafeAreaView components (or any hook like useSafeAreaInsets) inside the app.

  • Placement: Typically at the very top level of the app, above all other components.
    Example:

<SafeAreaProvider> <AppContent /> </SafeAreaProvider>

Without this, SafeAreaView won’t know the device insets and may not work correctly.


2. SafeAreaView

  • Purpose: It’s the component that actually applies padding/margin based on the safe area insets.

  • What it does:

    • Reads insets from the nearest SafeAreaProvider.

    • Adds padding (or can adjust margins) to ensure content does not overlap system UI (status bar, notch, home indicator, or navigation bar).

  • Props:

    • edges: Specifies which sides to respect (top, bottom, left, right).

      • Example: edges={['bottom']} → only adds padding at the bottom, ignores top/left/right.

<SafeAreaView style={{ flex: 1 }} edges={['bottom']}> <Content /> </SafeAreaView>

...


It does not seem to mention SafeAreaProvider at all.

but in its SafeAreaProvider page: https://appandflow.github.io/react-native-safe-area-context/api/safe-area-provider , it says, "You should add SafeAreaProvider in your app root component."

The documentation is not very precise. But I guess having a SafeAreaProvider will not create issues.

=========
The sequence of commands I used to create a production web deployment as well as release (production) APK:
npm run export-web
eas deploy --prod
cd android
$Env:CMAKE_VERSION = '3.31.6'
.\gradlew.bat assembleRelease

=============

10:43, 18 November 2025
In Web app:
Browse Courses button tap leads to: https://sg-eduapp-lms.expo.app/courses
  • which is: app\(app)\(authenticated)\courses.tsx
  • renders courses cards using CourseCard from '@/components/CourseCard' (components\CourseCard.tsx) not providing openLesson (undefined)
  • CourseCard has:
    • <Link href={openLesson ? `/course/${slug}/${openLesson}` : `/course/${slug}`} asChild>
  • As openLesson is undefined, app\(app)\(authenticated)\course\[slug]\index.tsx should be the link
    • when I tried it in web app for beginner's course (slug: beginner-course) it took me to: https://sg-eduapp-lms.expo.app/course/beginner-course which seems to match app\(app)\(authenticated)\course\[slug]\index.tsx code.
    • has Purchase Course or Start Course or Continue Course button (3 overloads)
    • Button click (if successful) leads to above course overview page.
      • Confirmed above for 'Start Course'. It took me to: https://sg-eduapp-lms.expo.app/course/beginner-course/overview/overview
      • Then completed one lesson and went back to Browse Courses -> Beginner's Course card -> Click: Showed me https://sg-eduapp-lms.expo.app/course/beginner-course with button text quickly changing from 'Start Course' to 'Continue Course'.
      • Clicking 'Continue Course' took me to: https://sg-eduapp-lms.expo.app/course/beginner-course/overview/overview. This is where I think UI can be improved by going to current lesson in course instead of course overview like the behaviour in 'My Courses' tab given below. But it is a minor issue.
      • Went to 'My Courses' -> Beginner's Yoga card (with 10% shown against it); link: https://sg-eduapp-lms.expo.app/my-content
      • Clicked Beginner's Yoga card -> Lesson 2 (current lesson of course); link: https://sg-eduapp-lms.expo.app/course/beginner-course/2
My Courses tab: app\(app)\(authenticated)\(tabs)\my-content.tsx
Tap on Card in above:
  • if lesson index is 0 -> app\(app)\(authenticated)\course\[slug]\index.tsx
  • if lesson index is non-zero -> 
    • if lesson index is overview/overview -> app\(app)\(authenticated)\course\[slug]\[lesson]\overview.tsx
    • else -> app\(app)\(authenticated)\course\[slug]\[lesson]\index.tsx
app\(app)\(authenticated)\course\[slug]\index.tsx and app\(app)\(authenticated)\course\[slug]\[lesson]\overview.tsx are similar but not same.
  • app\(app)\(authenticated)\course\[slug]\index.tsx code.
    • Screen is shown when user chooses a course card in Browse Courses screen: https://sg-eduapp-lms.expo.app/courses
    • has Purchase Course or Start Course or Continue Course button (3 overloads)
    • shows Course Overview text (this section of screen is same as in overview.tsx below)
    • has lesson list
    • Drawer is not shown
  • app\(app)\(authenticated)\course\[slug]\[lesson]\overview.tsx:
    • Gets invoked through /(app)/(authenticated)/course/${slug}/overview/overview which corresponds to web URL: https://sg-eduapp-lms.expo.app/course/beginner-course/overview/overview
    • has Start Course button going to 1st lesson of course.
    • shows Course Overview text (this section of screen is same as in course\[slug]\index.tsx above)
    • Does not have lesson list in right pane; However lesson list is available in drawer
-----------------
On Android app, once in a while, Course Overview page (of Beginner's course as well as Elevate course) shows image and title but content below that (course description) is blank. However Start Course button is shown at the bottom.

On web app, so far I have not seen this issue. Even now when the Android app is not showing the (course description), the web app is showing it.

On closing and reopening the Android app, the course description was shown at times!
But I think this is not a major issue as the other functionality of the app is not impacted.

I have used the Android app quite a bit now, opening and closing it multiple times:
  • Almost always, the bottom tabs are shown correctly. Just once I had the case of only top edges being shown. I then closed and reopened the app at which time it showed the bottom tabs correctly.
  • I have used 'Start Course' to initiate a new course on Android app multiple times. It worked as expected.
  • I think the only main use case remaining to test is to delete users from Clerk as well as Strapi and then see new user creation as well as user starting a new course (beginner's course) on Android app.
I have also used the web app to some extent. That seems to work OK.

Cleaned up code.
Tested local webapp - seems OK.

cd android
$Env:CMAKE_VERSION = '3.31.6'
.\gradlew.bat assembleDebug

BUILD SUCCESSFUL in 54s
1101 actionable tasks: 87 executed, 1014 up-to-date

Testing Android Debug APK on phone
Sometimes image is taking very long to load.
I deleted user-course entries on Strapi cloud
Was able to start Beginner's course through Browse Courses.
App seems to be working OK.

git commit -m "Android bottom tab visibility fixed; code cleanup"

=========================

https://strapi.io/blog/introducing-the-free-plan-for-strapi-cloud says, "Projects on the free plan scale down after a few minutes of inactivity and automatically spin back up when receiving a new request."
after how many minutes does Strapi cloud free tier scale down?
I did a Google Search ... ChatGPT did a search ... No luck.
Perhaps few minutes is as low as 5 minutes.
------------

Done: If there is a longish break while using the app, the Strapi Cloud server seems to go down. So the activity indicator within lessons/course also needs to have a message like the home page related activity indicator.

git commit -m "Refactored LoadingView to have innerScreen prop; Improved activity indicator message for inner screens"
======================

Me: In my React Native and Expo web app which uses Clerk for auth, I use Google SSO. But I would like to provide a guest mode where the user can use the app without using Google SSO. Does Clerk provide a guest user mechanism?

CG: ... Clerk doesn’t provide a first-class “guest user / anonymous session” feature out-of-the-box (as per their documentation). There is no built-in “anonymous session” object that Clerk treats like a signed-in user without credentials.
...

Me: The app which is a tutorial app heavily uses ClerkId . So changing app code to handle specific guest user case would be non trivial, I expect. Guide me on how to:
1) Create a dummy/guest user on Clerk
2) Login automatically to that dummy/guest user from the app
...

Me: Interesting! Let me share the login.tsx file of the app. Please modify it to incorporate the above guest being logged in the app on Guest button approach.

[CG provided modified login.tsx code. But I wanted to know how to manually create a user in Clerk. After some trials..] 

In Clerk to add guest user.
1) To get Create User button to do something:
  • Configure -> Email
    • Enable Sign-up with email 
    • Enable Require email address
    • Enable Sign-in with email
  • Configure -> Password
    • Enable Sign-up with password
2) Next I used Create User to create following user:
Email: guest@guest.com
Password: [Same as in CG provided code]

[CG provided modified login.tsx code is currently put in gem.txt. It seems very interesting and worth trying out.
But I decided to make a proper release of current code and then consider trying out modified login.tsx code.]

=================

npm run export-web
eas deploy --prod
  • As 4G net speed is slow - little before 17:25, 18 November 2025 - it failed once, retrying - it is taking time .... failed at the end ... Now trying with Jio ... speed is not great ... it was going backwards and forwards a few times - stopped it. Will do it later
cd android
$Env:CMAKE_VERSION = '3.31.6'
.\gradlew.bat assembleRelease
  • am trying this out ... don't know if this will need downloading and so crash or take just too long .... It has reached 82% Executing! Done quickly (see below). No downloading I guess.
BUILD SUCCESSFUL in 2m 24s
1454 actionable tasks: 92 executed, 1362 up-to-date
------------

Installed release APK on phone.
Bottom tabs are showing correctly in two opens of the app.
Will now delete my g ids from Clerk and also clean up Strapi backend. that creates proper ground for testing APK release before sharing it publicly.
Am trying eas deploy again, as I would like to test both Android and web apps together.

eas deploy seems to have gone through! 
🎉 Your deployment is ready

Dashboard       https://expo.dev/projects/--snip--/hosting/deployments
Deployment URL  https://sg-eduapp-lms--ud8k03xm15.expo.app
Production URL  https://sg-eduapp-lms.expo.app
----

But the command has not exited! ...  It finally exited!

Expo.dev shows a deployment at Nov 18, 2025 6:10 PM . That's this deployment.

Strange! https://sg-eduapp-lms.expo.app/ shows older loading message even after Ctrl+F5
But https://sg-eduapp-lms--ud8k03xm15.expo.app/ shows latest loading message.
Perhaps it takes some time for https://sg-eduapp-lms.expo.app/ to get updated with latest production deployment.

When I tried https://sg-eduapp-lms.expo.app/ again (within 10 to 20 minutes I think), it is showing latest loading message. So it seems to have got updated now.

18:23, 18 November 2025
Deleted my G users in Clerk dashboard
In Strapi cloud, deleted my G users, deleted all entries in User-course and Progress collections.

So now, from my G users perspective I am on a clean slate both in Clerk and Strapi. I don't think Stripe tx need to be deleted. Stripe dashboard is very, very slow.

Android release APK app test
============================
As dG user on Android (release) app on phone:
  • Sign Up
    • Done. Bottom tabs showed correctly. User got added to Clerk (35eW...)
    • Loading message took its time to go away but now I have a black screen with bottom tabs
    • Closed and reopened app. Now home screen image and text showed correctly.
    • But user did not get added to Strapi User collection!
  • Start Beginner's course
    • Was still able to start the course. Beginner's course with ClerkId 35eW user got added to user-course
  • Do few lessons
    • Did 2 lessons, Progress entries got added with ClerkId correctly
  • Sign out and close app
    • Done
  • open app again and login as dG
    • Did it. Loading message was shown correctly. Bottom tabs show correctly.
    • My Courses correctly shows 20% progress (2 lessons out of 10 or so) in Beginner's course
    • Browse Courses -> Beginner's course -> Shows screen with Continue Course button which correctly took me to Overview screen for course
    • Drawer shows tick against 2 completed lessons correctly.
    • Elevate Course correctly shows as purchase not available for Android
      • I think once it did not show the description text. I did not catch that correctly. Later it is showing.
  • Closed app without signing out
Now on mobile Chrome, went to https://sg-eduapp-lms.expo.app/
  • Logged in as dG user
  • Web app shows correct data
  • In Browse Courses -> Elevate Your Practice -> Purchase Course -> 
    • Payment screen ->(Dummy/Sandbox) Card: 4242 4242 4242 4242, Exp. date: 01/30, Sec. code: 222 -> Pay Now -> Payment Complete message -> Continue ->
    • Course Overview page for Elevate course is shown
    • So with mobile Chrome I was able to make Dummy payment and start Elevate course.
  • Profile -> Sign out
    • Shown Login screen
  • Closed browser tab.
Now opened Android app (on mobile)
  • It remembered dG login and took me to home screen (without having to login again)
  • My Courses -> Showed both Beginner's Course and (premium) Elevate course
  • Tapped Elevate Course -> Was shown Course Overview page of Elevate Course
    • -> Start Course button -> Was shown first lesson of "Refining Your Breath"
    • Tapped Complete & Next Lesson -> Was shown second lesson of "Inversions & Headstands"
    • Drawer shows tick mark against first lesson of "Refining Your Breath"
    • Went back to My Content (link at bottom of Drawer) (Should the link be My Courses?)
  • Shown 20% progress for Elevate course correctly (1 lesson completed out of 5).
  • Bottom tabs were being shown correctly throughout these tests of Android app
  • Short Loading message was also shown correctly at times when the inner screens were taking time to get loaded.
  • Profile -> Sign out
  • Was shown login screen.
The Android app has passed the above main functionality tests. It failed to add user to Strapi backend (perhaps due to poor network connectivity issues). I think the black screen (in dark mode) display mentioned for this test must be due to some failure condition which is not caught properly by the app and so no message is shown to user. But on restarting the app, it worked fine even without that user in Strapi user collection. That's probably because the code for the main functionality is ClerkId based which is supplied by Clerk auth on successful login of user.

Web app test on desktop PC
==========================
As regular G user (different from dG user used above on mobile), on web app on desktop Chrome browser, https://sg-eduapp-lms.expo.app/ :
  • Sign Up
    • Done. User got added to Clerk (35ed...)
    • Loading message was shown briefly after which home screen image and text showed correctly.
    • User got added to Strapi User collection
  • Start Beginner's course
    • Was able to start the course. Beginner's course with ClerkId 35ed user got added to user-course
  • Do few lessons
    • Did 3 lessons, Progress entries got added with ClerkId correctly
  • Sign out and close tab
    • Done
  • open app again and login as same user
    • Did it. IFIRC, loading message was shown very briefly and correctly.
    • My Courses correctly shows 30% progress (3 lessons out of 10 or so) in Beginner's course
    • Browse Courses -> Beginner's course -> Shows screen with Continue Course button which correctly took me to Overview screen for course
    • Drawer shows tick against 3 completed lessons correctly.
    • Elevate Course showed short (inner screen) loading message for long (Seems like a bug in web app).
      • I refreshed the page (https://sg-eduapp-lms.expo.app/course/advanced-yoga). Loading message showed briefly and then image, text and purchase button showed correctly (as purchase is available from web app)
    • Purchase Course -> 
      • Payment screen -> (Dummy/Sandbox) Card: 4242 4242 4242 4242, Exp. date: 01/30, Sec. code: 222 ->Pay Now -> Payment Complete message -> Continue ->
      • Course Overview page for Elevate course is shown
    • On Elevate course overview -> Start Course button (bottom of page; has to be scrolled down vertically) -> Was shown first lesson of "Refining Your Breath" (The active lesson is highlighted in left hand side drawer if the web window is wide enough)
      • Played video fully for this first lesson -> Was shown second lesson of "Inversions & Headstands"
      • Clicked 'Complete & Next Lesson' button -> Was shown third lesson of "Deep Hip Openers"
      • Drawer shows tick mark against first and second lesson correctly.
      • Went back to My Content (link at bottom of Drawer)
    • Shown 40% progress for Elevate course correctly (2 lessons completed out of 5).
    • Top Menu bar was being shown correctly throughout these tests of web app
    • Short Loading message was also shown correctly at times when the inner screens were taking time to get loaded.
    • Profile -> Sign out
    • Was shown login screen.
Progress and User-course collections in Strapi cloud had data as expected reflecting users who started the two courses and which lessons they completed.
==============

19 November 2025 (Early Hours)

Uploaded Release APK to GD.

Uninstalled app from phone. Downloaded from above link. Ran it using dG id. It worked OK.
Then signed in with Eklavya (not present in Clerk or Strapi prior to sign in).
User got created both in Clerk (35fD..) and Strapi.

Was able to Start Beginner's course
But had to tap 'Start Course' 3 times. I have faced it earlier too with this Release APK.
I deleted Strapi entries and tried 'Start Course' again. This time it needed a single tap!

Tried again with another user. Once again, only single tap needed!

Next time I face this slow response on first tap on 'Start Course', I should wait for some time to see if after some time it shows activity indicator. I have never faced this issue when the button is 'Continue Course' or 'Purchase Course'. 'Start Course' calls addUserToCourse (in StrapiProvider). Does that get stuck at times, due to which the state change in isProcessing to true triggers a late re-render (which is needed for 'Start Course' to be replaced by ActivityIndicator)?

Updated APK link on blog post and Readme.
Uploaded release APK to virustotal

======================
17:56, 19 November 2025

On Android app, lesson description is not shown!
Closed and reopened the app. Then lesson description is shown.

======================
23:24, 19 November 2025

Published post updates on FB, LinkedIn
Need to do one final clean test.
Deleted Clerk my G users
Deleted my G users in Strapi Cloud. Deleted all user-course and progress rows (all related to my G users only).
So now Clerk and Strapi Cloud are clean slate for my G users.

Uninstalled lms app from phone.
Downloaded APK from link provided in blog post: Interesting React Native Education (LMS) tutorial app (mobile and web) with public repos but not open source; Possibilities of open source equivalent, https://raviswdev.blogspot.com/2025/11/interesting-react-native-lms.html
Installed the app on phone (from APK)
Opened app but did not login and then closed app.
Will do main testing tomorrow.
====================

11:04, 20 November 2025
Testing with dG user sign up
  • Clerk user added (35jI..)
  • Strapi user added (35jI..)
  • Faced the Start Course button background changing on tap but Activity Indicator not being shown issue
    • Looked at expo.dev requests. The last POST request details are:
      • 200 POST /api/add-user-course Nov 19 01:00:50
      • So the expo.dev web app api did not get invoked.
    • Looked at Strapi Cloud backend log. There is no PUT/POST request to add to user-course
    • Also Strapi User-course collection is empty
  • Went to app again. This time, the first tap resulted in ActivityIndicator being shown. Also IFIRC, I tapped on the right side of the wide button, outside of Start text.
    • Now there is POST request at expo.dev
      • 200 POST /api/add-user-course Nov 20 11:19:44
    • Strapi server log has the entry:
      • [2025-11-20 05:49:45] [2025-11-20 05:49:45.670] http: POST /api/user-courses (285 ms) 201
      • Note time seems to be GMT. So 5:30 has to be added to it which makes it 11:19:45.670. That is a second after above web app api request which is as expected.
    • Strapi user-course collection has an entry for Beginner's course and 35jI.. ClerkID which is as expected.
    • The app itself showed the Beginner's course overview page
  • Chat with CG, "Button not triggering indicator", indicates that the UI/UX showing TouchableOpacity tap but onPress not being triggered is possible on Android. CG says solution is to use Pressable.
  • Tapping Start Course button on course overview page showed Introduction to Yoga lesson but with description as blank.
  • Tapping 'Complete ..' button took me to next lesson: Breath & Awareness. This time description was shown.
  • Chat with CG, "BlocksRenderer Lesson Notes Issue?", indicates that this intermittent issue faced mainly (or perhaps only) on Android release could be due to strapi BlocksRenderer which is a DOM based component.
-----------
Used web app to sign up new user, start course and go through lessons. Web app worked without any issues (lesson text and overview text was shown all times).

===================================================
===================================================

Details

Me: ... BTW Some NGOs get some free quota on AWS and Azure, I believe. Such an NGO can host the STRAPI server on their free quota AWS/Azure and get good first and later response time for STRAPI requests sent to that STRAPI server. Am I right?

CG: Short answer: Yes — absolutely.

If an NGO gets AWS/Azure free credits or free-tier eligibility, they can host Strapi there with excellent performance and no cold starts, as long as they choose the right hosting model.

Here’s the clear picture:


1. AWS or Azure free quota is perfect for Strapi

A Strapi server needs:

  • A running Node.js instance

  • A database (SQLite/PostgreSQL/MySQL)

  • Optional file storage (S3 / Azure Blob)

If you use AWS/Azure free-tier:

✅ You can keep the VM always running
✅ No cold starts
✅ Much faster than Strapi Cloud Free
✅ Startup only happens once at VM boot
✅ Requests will usually respond in 50–300 ms, not 1 minute

This is exactly how many startups & NGOs deploy Strapi.


2. Recommended AWS setup (free-tier friendly)

Use:

EC2 t2.micro or t3.micro (free for 12 months)
✅ Amazon Linux / Ubuntu
✅ Node.js
✅ Nginx reverse proxy
✅ PostgreSQL on Amazon RDS (free-tier) OR SQLite

Performance:

  • Requests: very fast (100–300 ms)

  • No cold starts

  • Very stable

  • Can handle decent traffic

This is far superior to Strapi Cloud Free Tier.


3. Recommended Azure setup (free-tier for NGOs)

Azure offers credits to NGOs via the Microsoft Azure Nonprofit program.

With these credits you can run:

✅ Azure VM (B1s or B1ls — very cost-efficient)
✅ Azure PostgreSQL (if needed)
✅ Blob Storage for images
✅ Optional Azure App Service (can run Node)

Performance:

  • Similar to AWS: stable, warm, good latency

  • Node.js apps run very well on Azure App Service


4. Full-time VMs eliminate cold starts completely

If the NGO runs Strapi on a VM:

✅ The server never sleeps
✅ No cold boots
✅ No 1–2 minute delays
✅ App responds instantly after deployments
✅ Can keep server warm with small cron scripts or PM2

This matches your expectation:

“good first and later response time for STRAPI requests”

Yes — both first and later responses will be fast.


5. Important note

If hosting Strapi on AWS Lambda or Azure Functions (serverless),
you will still get cold starts — sometimes worse.

So avoid:

🚫 Lambda
🚫 Azure Functions
🚫 Cloudflare Workers
🚫 Vercel Functions

Strapi is not ideal for serverless architectures.

✅ Use full-time VMs (EC2 / Azure VM)
✅ Or use App Service plan with “Always On”


6. Many real NGOs already run Strapi on AWS/Azure

Examples:

  • Education nonprofits

  • Community platforms

  • Small media publishers

  • Cultural/heritage organizations

They use Strapi as:

✅ CMS for websites
✅ Backend for mobile apps
✅ Enrollment/course APIs
✅ Internal dashboards

And AWS/Azure free quota + grants make this cost-effective.


Conclusion

✅ Yes, NGOs using AWS/Azure free quota can host Strapi with:

  • Much faster initial responses

  • No cold starts

  • Better reliability

  • Better scaling

Your understanding is correct.


If you want, I can:

✅ Suggest the best AWS/Azure architecture for your exact Strapi app
✅ Give a deployment guide (minimal commands)
✅ Compare Strapi vs PayloadCMS/Sanity/Firebase if scalability is a concern

Just let me know.

Me: Terrific info. Very useful for me. Thanks a ton!

CG: You're most welcome! I'm really glad this information helped — and if you ever want to explore:

✅ How to deploy Strapi on AWS/Azure
✅ How to tune performance
✅ Whether you should move to a different backend later
✅ Best architecture for React Native + Web + CMS
✅ Cost estimates for NGO scenarios

…just ask anytime.
Happy experimenting and learning — you're doing great progress!

=======================

Me: I want to now install and test the apk on android emulator. My android emulator setup frequently complains of not enough space to install an apk. 
...

Me: [Settings -> Storage] It shows 7.7 GB used out of 8 GB total. Hmm. That's the issue. Apps takes up 5.0 GB but almost all of that is standard apps preinstalled in the emulator.
...

Me: I cleared storage of Google Play Services (ignoring warnings - after all, its only an emulator; I can recreate it if there are issues). Now storage space is 6.5 GB used out of 8 GB

CG: ... The Play Store will eventually auto-update:
  • Chrome

  • WebView

  • Play Services

These updates can add hundreds of MB.

If that happens, you may again see low storage.

[CG was suggesting increasing memory or whatever its called of emulator device to 16-32 GB as a good long-term solution.]

Me: how can I disable auto update on the emulator.

CG: 

✅ Method 1 — Disable auto-updates inside Play Store (works partially)

  1. Open Google Play Store inside emulator

  2. Tap your profile icon (top right)

  3. Go to Settings → Network preferences

  4. Tap Auto-update apps

  5. Select Don’t auto-update apps

This prevents most app updates, but Google Play Services may still update occasionally.

...

✅ Method 3 — Completely block updates using Metered Network trick (best)

Android will never auto-update apps on metered connections.

So set your emulator’s Wi-Fi to metered:

  1. Settings → Network & internet → Internet

  2. Tap the connected Wi-Fi (AndroidWifi)

  3. Tap Network usage

  4. Set Treat as metered

This prevents:

✔ Play Store auto-updates
✔ Google Play Services updates
✔ WebView updates
✔ Chrome updates

This method is the most reliable.


Optional extreme method

If using Android Studio’s emulators with Google Play:

Switch to a non-Google-Play system image (AOSP)

Google Play Services won’t exist → no updates → tiny storage usage.

But apps requiring Google SSO won't work in AOSP images.

Since you are using Clerk + Google SSO, you must keep Google Play image.
So, stick to the earlier methods.

---------
[IFIRC, I did method 1 and method 3 above.]
...
Me: I increased Internal storage by 1024 MB to 7168 MB in Device Manager.

CG: Great — that was the right step, but note one important point:

🔧 **Increasing “Internal Storage” in Device Manager modifies the AVD config,

but it does NOT resize the already-created virtual disk.**

So the emulator still uses the old 8 GB disk unless you recreate it or explicitly wipe/resize it.


✅ To make the new storage size actually take effect

Option 1 (Recommended): Recreate the emulator

...
[IFIRC, CG was right. My increasing internal storage did not increase storage space shown on emulator (even after emulator restart).]

Me: Noted. Now I don't want to recreate the emulator. but I will bear it in mind for future.
BTW another issue is that the emulator almost always fails to load the snapshot. Its messages:
INFO         | Loading snapshot 'default_boot'...
WARNING      | Device 'cache' does not have the requested snapshot 'default_boot'

WARNING      | Failed to load snapshot 'default_boot'
USER_WARNING | The saved emulator state could not be loaded, performing a cold boot.

CG: This is a very common and harmless issue with Android emulators, especially the Google Play ones.

Snapshots break easily after:

  • Updating Android Emulator version

  • Updating system images

  • Changing hardware profile (RAM/Storage)

  • Changing graphics settings

  • Crashes / failed shutdowns

That warning basically means:

“The snapshot is invalid or missing → doing a cold boot instead.”

So the emulator works — it just boots slowly.

...

🟦 Why snapshots fail so often

Google Play images contain Play Services, which auto-update in background.
Snapshot expects a consistent FS state — but Play Services changes files → snapshot becomes invalid.

That’s why:

💡 Google Play AVDs = frequent snapshot corruption
💡 Non-Play AVDs = stable snapshots (but no Google SSO)

...
Me: Got it. Then I wont bother about snapshot failure. Anyway it cold boots everytime. So I will go with that.
...

Me: Despite setting it as metered network, the emulator is doing lot of downloading. I think it is updating all the Google Play Services data I deleted.

...
Me: I turned off WiFi on the emulator. That seems to have stopped the downloading.

[Sometime earlier I had downloaded Google Drive APK file using shared link on Android emulator.]

Me: Hmm. I installed my app on the emulator. Even with WiFi off, I was able to do Google SSO and then (after free-tier Cold Start Strapi delay), the home page showed. The emulator is not doing any downloading. Hmm. I think it is using 'LTE' now and since I have set the connection to metered, it is using Internet only through the app and is not doing auto updates via the Internet connection.
...

Me: Note that if WiFi is on, then the metered setting does not come into play and auto update seems to happen.
...

Comments