Notes on learning stage of developing my own React Native education app using Expo Framework - Part 6
For the Expo workflow you are using:
with:
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)
✅ 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
onLoadEndfires
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=falseon 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:
✔ isLoading → first load
✔ isFetching → background refetch
✔ Works on Web + Android + iOS
✔ No need for Platform.OS === 'web' ? false : true
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.
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.]
build.gradle is already handling these settings properly, using Expo/React Native defaults.
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:
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:
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:
That C_ is a known CMake path normalization bug.
✅ Why your fix worked
You set:
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
(Works on recent AGP versions.)
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:
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:
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.
=======================
✅ 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.”
All three apps (LMS, CrudApp, StickerSmash) have:
So Android sees all three apps as valid handlers for any deep-link like:
Therefore, after your Google SSO login finishes and redirects to:
Android correctly shows this dialog:
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:
or slightly more unique:
Do NOT use myapp for production apps.
It is too generic and will conflict on your phone and on users' phones.
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:
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 app — NO 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.
✅ Your code does not specify a redirect URL manually.
When using:
Clerk Expo automatically builds the redirect URL from the Expo app scheme.
That means:
Clerk builds redirect URL as:
So if your app.json is currently:
then your SSO redirect URL is:
🔥 Why this matters
Since all three apps (CrudApp, LMS, StickerSmash) use:
they all have the same redirect URI registered on the device.
So when Google finishes login and tries to open:
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:
Then Clerk will automatically use:
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 withlms://.” -
Clerk OAuth returns the SSO redirect to:
-
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.
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.
✅ create-expo-app always defaults the scheme to "myapp"
Here is the definitive conclusion.
✅ 1. You have no deep-linking config except the single scheme field
Why?
Because for mobile Expo/React Native apps Clerk automatically allows custom-scheme redirects ...
Here’s why:
-
The scheme change in
app.jsonaffects 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 usingexpo-routeror similar routing. -
Creating a fresh web distribution ensures that:
-
All deep link handling and routing is consistent with the new scheme.
-
Any potential cached URLs or old builds don’t interfere.
-
You can confirm that the Clerk login / SSO flow still works correctly in the web version.
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.
email field in the Users table has a UNIQUE constraint.❗ 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:
or
But your actual schema is:
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.
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.✅ Your mobile production APK is calling a relative URL:
On Android production build, this becomes:
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
Your production Android APK is trying to call:
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/
...
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:
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
...
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?
...
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.
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
⭐ 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.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.
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 runs →
cancelled = 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 ...]
<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"
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-courseentry 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-coursesucceeded.
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. ✅
...
<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
SafeAreaViewcomponents (or any hook likeuseSafeAreaInsets) inside the app.
-
-
Placement: Typically at the very top level of the app, above all other components.
Example:
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.
-
-
- 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
- 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 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
- 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.
- Configure -> Email
- Enable Sign-up with email
- Enable Require email address
- Enable Sign-in with email
- Configure -> Password
- Enable Sign-up with password
- 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
- 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.
- 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
- 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.
- 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.
- 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.
- 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.
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.
✅ 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!
=======================
Chrome
WebView
Play Services
These updates can add hundreds of MB.
If that happens, you may again see low storage.
✅ Method 1 — Disable auto-updates inside Play Store (works partially)
Open Google Play Store inside emulator
Tap your profile icon (top right)
Go to Settings → Network preferences
Tap Auto-update apps
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:
Settings → Network & internet → Internet
Tap the connected Wi-Fi (AndroidWifi)
Tap Network usage
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.
🔧 **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
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)
Comments
Post a Comment