Using ChatGPT and Gemini to write React Native and Expo Timestamp Tracker app (mobile and web)


Last updated 29 Jun 2025

Quick Info

Timestamp Tracker App for Android and Web Platforms
The Uptodown app store page for this app on Android has certified it as free from viruses, spyware, and malware. Note that the app store has advertisements with their own download buttons. In the Uptodown app store page that is shown on clicking the 'uptodown' link button below, click on 'Get the latest version' button to download this app for Android. Alternatively you can download the Android app directly from my Google Drive link also given below.

Download Timestamp Tracker

Android APK download from Google Drive

This Timestamp Tracker app is a launch and one-touch-add timestamp recorder app with facility to add a note to any timestamp entry. It automatically creates a timestamp when the app is launched. As it has been developed using React Native and Expo, there is an Android app version and a web version. As I do not have access to a MacOS computer nor an iOS phone/tablet, I did not create an iOS app.


Two screenshots of Android app running on my Samsung M21 phone with device setting of light mode. Left screenshot shows screen without milliseconds display (MS Off) and right screenshot is with milliseconds display (MS On). Note that these screenshots may be of an older app version but with screens similar to current app version. 
[Chrome browser: To open pic in larger resolution, on PC right-click on pic followed by open link (NOT image) in new tab/window and on mobile, press pic and choose Open in new tab. Then switch to new tab/window and on PC you may have to click on pic to zoom in.]

Edit timestamp note modal on Android app on phone.


Screenshot of web app running on my desktop Windows 11 PC in Chrome browser with setting of dark mode. Note that these web app screenshots may be of an older app version but with screens similar to current app version. 

Edit timestamp note modal on web app.

App Features

This Timestamp Tracker app is a launch and one-touch-add timestamp recorder app with facility to add a note to any timestamp entry.
It automatically creates a timestamp when the app is launched. 
It has buttons to:
  • Add a timestamp
  • Export timestamps data as .csv
  • Show/hide milliseconds
  • Clear the timestamps (some or all timestamps)
  • Show app info.
It also shows duration from previous timestamp and if a note has been added to the timestamp entry, it shows the initial part of the note. 
Tap/Click on timestamp entry shows a modal to view/edit note. Maximum note length is 500 characters.

The app will display a maximum of 100 timestamps. A warning is shown when user approaches the limit - timestamps full. When a timestamp is attempted to be added but the timestamps data limit has already been reached - timestamps full - a suitable message is shown. User has the option to clear some or all timestamps after which new timestamps can be added.

This app is useful to quickly and easily record timestamps and also to capture typically small break duration or even small task duration. The note facility provides a way to record what the associated activity was. The app supports light and dark modes and uses the device settings for it.

Note that if MS Off (hide milliseconds) option is chosen, milliseconds are still shown in Edit Note modal. Further, the interval calculation still uses the milliseconds, and rounds the seconds figure based on milliseconds difference. This results in interval sometimes being different by 1 second from simple subtraction of the later timestamp (rounded seconds) from its immediately previous timestamp (rounded seconds). For more precision, use MS On (show milliseconds) and in this case, interval will be same as subtraction of the later timestamp from its immediately previous timestamp.

The Export csv file for both options of MS On and Off (Show and hide milliseconds) has timestamp information (with or without milliseconds) in format suitable to be read by Microsoft Excel as a date time value. User can then do subtraction of Excel date-time cells to get the time interval as hours, minutes, seconds (and optionally milliseconds). The Excel cell formats to show such time interval values are:
  • Shows milliseconds: [h]:mm:ss.000 
  • Does not show milliseconds: [h]:mm:ss
Showing days in time interval seems to be complex in Excel. So a 50 hour difference using above formats will show as 50 (hours) and not 2 days and 2 hours.
This feature of exporting data via csv to Excel allows the user to easily remove unwanted entries and so focus only on needed entries. Further intervals between non-consecutive timestamps can be easily calculated using suitable Excel simple cells subtraction formula.

The app was developed by me with assistance from free-tier ChatGPT and free-tier Gemini.

Key links related to the Timestamp Tracker app
  • Android app page on Uptodown App Store which has certified the app as having no virus, no spyware and no malware. Uptodown store has advertisements with their own download buttons. In the Uptodown app store page that is shown after clicking the 'uptodown' button below, click on 'Get the latest version' button to download this app. Click on virus shield icon there to see security report of this app.
    Download Timestamp Tracker
  • GitHub repo (public)
  • Android release APK in public Google Driver folder (57.2 MB)
    • This release APK has been scanned using VirusTotal, a reputable online service that aggregates antivirus engines and online scan results. The scan reported the APK file to be clean i.e. it does not contain any malicious viruses. You can view the full VirusTotal scan report for the app's APK here: https://www.virustotal.com/gui/file/55d7c63e167b78c4fcb09310d68b654fbb9a9708bb82577ab8a1235f6157d41a . This scan provides an independent verification that the app is free from known malware and other threats.
  • Web app deployed on GitHub Pages
Wish/To Do List
  1. Add 'Settings' button with:
    1. Option to show/hide Interval.
    2. Set the maximum number of timestamps.
  2. Add Search feature.
  3. If needed, consider multiple selection of timestamp entries followed by single delete operation to delete selected entries.
  4. If needed, consider adding Import feature.
Background

I had written two very simple apps, one a web app (in Dec. 2024) and the other an Android app (in Feb. 2025), that did a one touch timestamp recording. Probably in end 2024, I had done a limited search on Play Store for such an app but I could not get a free Timestamp app that did what I wanted as a one-touch operation. One app I tried needed three touches - to launch the app, to initiate adding a timestamp and finally a third touch to create the timestamp (with empty text). So I wrote the above mentioned apps.

As now I am learning React Native and Expo, I wondered if I could take ChatGPT's help to create a single source code base app that worked both on web and on Android. That led to the app covered in this post.

For more about the earlier apps: 
  1. Dec. 2024: Very Simple Launch and Refresh Timestamp Recorder Web App, https://github.com/ravisiyer/LaunchTimestampWebApp
  2. Feb. 2025: Very Simple Launch and One-Touch-Add Timestamp Recorder Android App, https://raviswdev.blogspot.com/2025/02/very-simple-one-touch-timestamp-on.html

Details

This is a very long section which may only be useful to developers. It tries to capture the procedure I followed to develop the app with free-tier ChatGPT and free-tier Gemini's help. It also covers some problems I ran into.

This post has the first part of details. The second part post: Continued ... React Native and Expo Timestamp app (mobile and web) - part 2.

The first phase of this activity was from 27th to 29th May 2025. Then there was a break of a few days after which I continued the work.

27th to 29th May 2025

For this work, my first message to ChatGPT:
Write a react native program that does the following:
1. Shows a timestamp having the time the app is launched or opened.
2. Has a button that adds the current timestamp.
3. Stores last 100 timestamps in local storage and displays all the stored timestamps.
4. For every 2 consecutive timestamps shows the time interval between them.
5. Has a button to clear all timestamps on confirmation of the action.
----

Later, I clarified the above requirement:
Give me a version of the app which works on native as well as web. I think Expo would be needed for that.
----

ChatGPT (CG) generated a project zip file but I got some errors with it. It is possible that the errors cropped up as ChatGPT tried to add Expo to the earlier project it had generated without Expo. After a couple or so failed tries to get an error free build/run, it suggested "restart from a clean, working base that supports Web, Android, and iOS", which I followed. The final steps (omitting some error and retry iteration(s)) are given below. The source code of the project is shared publicly:  https://github.com/ravisiyer/CGPTTimestamp .
  1. npx create-expo-app TimestampTrackerWeb --template
  2. Choose blank template
  3. npx expo install @react-native-async-storage/async-storage
  4. Replaced App.js with App.js version provided by CG.
  5. npx expo install react-dom react-native-web @expo/metro-runtime
  6. npx expo start --web
The web version ran and seemed to have got the basic functionality working. But Clear All was not working. The button sizes and colours part of the user interface were not great but I wanted to focus on functionality first and later look at UI niceties.

CG gave a fix for the Clear All which worked.

I next tried it on Expo Go on my Samsung Android phone. I had to upgrade its Expo Go app to latest SDK version 53. The app worked on the phone. It worked on Android emulator too (whose Expo Go app IFIRC also had to be upgraded to SDK version 53).

CG offered some additional steps of which I asked for dark mode and export to file features. It provided modified App.js. I also had to run:
npx expo install @react-native-async-storage/async-storage expo-file-system expo-sharing

On trying it out, dark mode worked on web but not on android device. Later I noticed that export did not work on web.

Till this point, I think I did either no coding on my own or very minimal coding. I did not even read the generated code in detail though I did have a quick overview of it to confirm that the code looked OK and that when needed, I could get into its details. I think what I have done so far, largely, if not fully, fits the term, "vibe coding", as defined by wikipedia: https://en.wikipedia.org/wiki/Vibe_coding . While I had heard the term earlier and had some vague idea about it being related to code from AI, I read up and viewed up on it only after I did the above work. My intent was to see how much I can get out of free tier ChatGPT without getting into coding myself, and that intent, I thought may be similar to "vibe coding" though I did not know what it exactly was. I learned later that my above work seems to match "vibe coding".

Last commit till this point: "export timestamps; dark mode working on web but not android"
=============
=============
7 to 8 Jun 2025

Me: Let me add a Timestamp to this chat here: 2025-06-07 19:01:39
Got CG's help to fix dark mode issue in Android.
The issue was app.json entry of: 
"userInterfaceStyle": "light"
---
That had to be changed to:
  "userInterfaceStyle": "automatic",
---
followed by: npx expo start --clear
----

I think this was the first time in this work that I partially edited a file. Prior to this, I was opting for CG to provide me full code of a file and then just replacing existing code in the file with CG provided code. After this partial edit, I did many other partial edits though I did use full file code too at times. I guess that is not (fully) 'vibe coding' and probably the right term for this part of the work is just CG assisted coding.

Me: The package.json change fixed the dark mode issue for Android. Checked that dark mode/light mode works now both on web and Android. However, on Android in dark mode, the status bar is not shown.

CG suggested the fix using StatusBar component, which worked.

Next CG gave fix for Export File on web by having different code for web and Android.

I had to start a new chat a couple or so times as I had uploaded a file or had an image file in the chat, which led to ChatGPT app. forcing me to stop the existing chat on my free tier plan. But it allowed me to switch to a new chat. I should bear in mind that file upload or image download brings in this limitation of the chat getting stopped after some time.

----
With CG's help, got interval formatted as days hrs mins secs.
Next, got an icon image from CG. It was 1 MB in size.
----
CG: 📉 Typical icon sizes in lightweight apps:
SizeIdeal Use Case
<100 KBCommon for minimalist, clean icons
20–50 KBExcellent for performance and size goals
>100 KBAcceptable for detailed or colorful icons
----

With CG's help:
Compressed it to 500 KB odd using TinyPNG, https://tinypng.com/ [CG said Win11 Paint does not have compress feature.]
Reduced its resolution from 1024x1024 to 256x256 using Paint
["Resize" in toolbar (or Ctrl + W) -> Choose "Pixels" -> Set both Horizontal and Vertical to 512 or 256]
That resulted in icon file size becoming 59KB.
--------
CG:
A 256×256 PNG at 59 KB is a solid balance — especially for a minimalist app like yours. 👍

✅ Why it's a good fit:
✅ Small enough to keep your app lightweight

✅ Big enough for quality display on most devices

✅ Works perfectly with Expo (it automatically scales icons for various resolutions)
-------------------
Timestamp app has come up to minimal needs. So want to make a release at this stage.
Am trying to do local android build. EAS local build is also possible but I did not catch that in CG's message. So I asked for local build without EAS. Perhaps next time I have to do such a build for another RN app, I could consider EAS local build. Note that EAS cloud/server build queue can sometimes be many hours and so I wanted to avoid that.

EAS local build info. provided by CG is given below.
-------
To do a local Android build of your Expo app (APK or AAB), you'll need to use EAS Build, which supports local builds with the --local flag.

Here’s your step-by-step guide for a local Android build:


✅ 1. Install EAS CLI

If you haven't already:

npm install -g eas-cli

✅ 2. Configure EAS Build (if first time)

Run this to create or update your eas.json file:

eas build:configure
  • Choose Android only

  • Accept defaults (or customize later)

It creates a eas.json file in your root project folder.


✅ 3. Add Android package in app.json

You've already done this:

"android": {
"package": "com.yourname.timestamptracker" }

✅ Good.


✅ 4. Run the Local Build

Now run:

eas build --platform android --local

This builds an .apk or .aab on your own machine, not Expo’s servers.

  • ⚠️ This will require Android Studio’s build tools and Java JDK 17+ installed and in PATH.
  • Let me know if you want me to verify your environment or guide you in setting it up.


✅ 5. Result

Once complete, EAS will show a path like:

Build successful
Output: /path/to/your/build.apk

You can then:

  • Transfer APK to your Android device via USB or PairDrop

  • Install it manually to test


Optional: To force APK (instead of AAB):

eas build --platform android --local --profile preview

In eas.json, define:

{ "cli": { "version": ">=3.0.0" }, "build": { "preview": { "android": { "buildType": "apk" } } } }
--------
CG:

Expo needs a unique package ID for Android builds:
"android": {
  ...
  "package": "com.yourname.timestamptracker"
}

Replace yourname with your name or initials. This is essential if you plan to:
  • Use EAS Build for .aab or .apk

  • Publish to Google Play

----------
Added to app.json:
      "package": "com.ravisiyer.timestamptracker"
----
Me: I think I don't want to use EdgeToEdge as that may trip up at times.
Also, assetBundlePatterns is new to me and so I would like to skip it.
CG was OK with above.
----
CG:

What you need to do for Android local build on your PC

  1. Check your Expo workflow
    If you are in the managed workflow, you cannot build local native binaries directly without ejecting/prebuilding.

    To eject/prebuild:

    expo prebuild

    This generates native Android/iOS projects in android/ and ios/ folders.

  2. Install Android SDK and tools
    You said your PC already has the required software, but confirm:

    • Android Studio installed

    • Android SDK and platform tools installed

    • ANDROID_HOME environment variable set

    • Java JDK (usually Java 11) installed

  3. Build using Gradle
    Go to your project’s android folder and run:

    ./gradlew assembleDebug

    or for release build:

    ./gradlew assembleRelease

    This produces APKs in android/app/build/outputs/apk/.

  4. Sign your APK (optional, for release)
    You’ll need to sign the APK to publish on Play Store.


How your current app.json fits in:

  • The app.json config is used by Expo and EAS Build.

  • For local native builds, Android Studio reads the configuration from native files (AndroidManifest.xml, build.gradle, etc.) generated by expo prebuild.

  • Your app.json still matters for expo prebuild to generate the native projects.


Summary of next steps

  • Run expo prebuild to generate native projects (if not already done)

  • Open android folder in Android Studio or use command line Gradle to build APK locally

  • Test APK on your device/emulator


Example commands:

# Generate native projects from your managed Expo project
expo prebuild # Navigate to android directory cd android # Build debug APK ./gradlew assembleDebug # Or build release APK ./gradlew assembleRelease
---------------------------

About Gradle versions in React Native Android builds:

  • The Gradle wrapper (./gradlew) used by React Native projects downloads and uses its own Gradle version, independent of any Gradle installed globally on your PC.

  • This is by design to ensure everyone builds with the same, compatible Gradle version regardless of local setup.

  • So running ./gradlew assembleDebug will try to download the version specified in android/gradle/wrapper/gradle-wrapper.properties (in your case, gradle-8.13-bin.zip).


Can you use your already installed older Gradle?

  • Not really, because ./gradlew explicitly uses the wrapper version, ignoring your system Gradle.

  • You could manually install an older Gradle version globally and run gradle assembleDebug instead of ./gradlew, but this is not recommended because:

    • The React Native build scripts expect the wrapper’s Gradle version.

    • Compatibility issues may occur.

    • The wrapper manages plugins and dependencies tightly coupled to that Gradle version.

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

I ran: npx expo prebuild
Above command gave some warnings. One was: " android: userInterfaceStyle: Install expo-system-ui in your project to enable this feature."
CG recommended install. So I did: npm install expo-system-ui
CG said:
 For basic usage, you don’t need to change your App.js code just because you added expo-system-ui. ... expo-system-ui works mostly behind the scenes to apply the system UI colors that match your app’s theme. ... Since you already have the <StatusBar> configured in your App.js (with barStyle and backgroundColor based on dark mode), it should now work better and more consistently.

Ran: cd android, followed by: ./gradlew assembleDebug
That involves downloading lot of gradle stuff (it does not use previously installed gradle stuff for some reasons as per CG which are given above). I am using Jio SIM. Download speed seems to typically be btw 1 to 3 Mbps.
Now it is 9.45 PM. It has taken 26m already and is at 9% Executing stage.
...
BUILD FAILED in 45m 51s ... Could not get resource 'https://repo.maven.apache.org/maven2/com/facebook/react/react-android/0.79.2/react-android-0.79.2-debug.aar'.
            > Read timed out
----
Retrying. It went very quickly to above download again (213 MB) and shows 9% EXECUTING [56s]
BUILD SUCCESSFUL in 24m 43s.
----
Great! So though it took around 46 + 25 = 71 mins between 9 and 10.30 PM, it got done! Now to check if it works.
---------

...TimestampTrackerWeb\android\app\build\outputs\apk\debug\app-debug.apk is the file! It is 121 MB.
I was able to easily transfer the file using pairdrop.net from Desktop PC (DPC) to mobile. Actual file transfer took around 29 seconds (around 4.2 MB/s). It was much more convenient than USB tethering.

--------
Me to ChatGPT:
The debug build took around 71 minutes with my 4G mobile Internet providing a typicaly download speed during this peak period between 1 and 3 Mbps. The build got terminated once due to read timeout but when I restarted it, it seemed to quickly pick up from where it had stopped the last time. TimestampTrackerWeb\android\app\build\outputs\apk\debug\app-debug.apk is the file! It is 121 MB. I used pairdrop.net to conveniently transfer it to my phone. This file transfer took only around 29 seconds (around 4.2 MB/s). It was much more convenient than USB tethering. Thanks for your previous chats on using pairdrop.net.

Now I have installed the debug build on my Samsung Android phone. For running it, I need to have Metro running in development mode on my PC, isn't it?
-----------

On both Samsung phone and Android, with Metro running on PC (as development build), when I open the app, it opens and then closes immediately. At times, I am shown the error, "Timestamp Tracker keeps stopping"

CG first said, "This usually points to a React Native development client misconfiguration, especially when using Expo + custom debug builds." This was where trying out CG's main suggestions sucked up a lot of my time but did not deliver the solution. Given below is a list of the failed suggestion:
  1. You should not use plain ./gradlew assembleDebug if your app depends on Expo-specific modules like expo-file-system, expo-sharing, expo-system-ui, etc. Instead, use: npx expo run:android . [Note that CG had suggested ./gradlew assembleDebug first and I had followed that. Anyway npx expo run:android did not solve the problem.]
  2. "JS runtime error on launch (during useEffect)" ... CG suggested console.log statements on launch of app which I added. But these console log statements did not appear (in Metro) for Android app (but appeared for web app in browser console).
  3. "Clear potentially corrupt AsyncStorage". I tried that by uninstalling and then (re)installing the app. It did not fix the problem.
Then CG suggested using adb (in another terminal window) and gave an additional suggestion of running 'npx expo doctor'. The adb suggestion was interesting as it is, IFIRC, the first time I am using it. I did not try the 'npx expo doctor' suggestion as the adb suggestion itself took lot of my time and I had many interactions with CG on it. CG, IFIRC, did not repeat the 'npx expo doctor' suggestion and so I did not try it then (eventually it was npx expo-doctor (slight variation in command) that led to the solution).

adb showed a huge number of messages including when the Android app was started and which closed very quickly. CG suggested "adb logcat > crashlog.txt" followed by searching the file for "FATAL or your package name". After some tries, I got a "--------- beginning of crash" line in the output and copy-pasted many lines from there to CG. CG zeroed in on the error. 
❌ Root Cause [CG shows the error message:]
java.lang.NoSuchMethodError: No virtual method getConverters()
  in class expo.modules.kotlin.objects.ObjectDefinitionBuilder
-------
CG continues: This happens inside:
expo.modules.systemui.SystemUIModule.definition(SystemUIModule.kt:83)
💣 This is due to a version mismatch between Expo SDK modules, specifically the expo-system-ui module depending on a newer version of the expo-modules-core (which contains ObjectDefinitionBuilder with getConverters() method), but your project is using an older version of expo-modules-core.
...
You likely upgraded/downgraded some modules manually or added one (expo-sharing, expo-file-system, etc.) that pulled in mismatched versions.
-----------
I responded: Yes I did do npm expo-system-ui after npx expo prebuild suggested that. Then I asked you whether I should run npx expo prebuild again. You said it is not required.

After this, CG was not able to help much. It suggested uninstall of expo-system-ui followed by 'npx expo install expo-system-ui'. Note that free tier CG's data is a few years old. So Expo versions it seems to know well are 50.x.x and 51.x.x whereas I was using 53.0.9. CG said, "In your case, expo-system-ui@5.0.8 is not compatible with SDK 53 — it expects Expo SDK 50." I think that was a wrong statement. Further, it put me on the wrong track which sucked up some time.
-----------
Some info. about me trying to dig into the version issue.
package.json:
    "expo-system-ui": "^5.0.8",
----
npm uninstall expo-system-ui
npx expo install expo-system-ui
package.json:
    "expo-system-ui": "~5.0.8"
---

TimestampTrackerWeb> npm ls expo-system-ui
timestamptrackerweb@1.0.0 C:\Users\ravi-user\NotInBackupScript\CGPT\TimestampTrackerWeb
└── expo-system-ui@5.0.8

I chose not to do 'npx expo install expo-system-ui' as I felt that package was not the issue. Instead I went for the rebuild option which would, or rather should, pull in the right packages anyway.

I deleted node_modules, android and .expo folders (no .expo-shared folder was present).
Then I asked CG: Should I run npm install 
OR
npx expo install (or whatever the expo related command is)?

CG said: You should run:
npm install

Explanation:

  • npm install reads your package.json and installs all dependencies listed (with versions like ~53.0.9).

  • npx expo install is typically used to install new Expo-compatible packages, as it ensures the correct versions for the current Expo SDK.

    • Example: npx expo install expo-camera (when you're adding expo-camera for the first time).

Since you've already got everything in package.json, and just wiped out node_modules, you now just want to restore your existing deps. So:

👉 Run:

npm install
--------------

I ran npm install.
Then I ran: npx expo prebuild --platform android
Its output messages:
√ Created native directory
› Using react-native@0.79.2 instead of recommended react-native@0.79.3.
√ Updated package.json | no changes
» android: EDGE_TO_EDGE_PLUGIN: No configuration found for edgeToEdgeEnabled field in the project app config, falling back to false. In Android 16+ (targetSdkVersion 36) it will no longer be possible to disable edge-to-edge. Learn more: https://expo.fyi/edge-to-edge-rollout
√ Finished prebuild
===============

I specifically asked CG: Is this a problem: "Using react-native@0.79.2 instead of recommended react-native@0.79.3."

CG:

No, it's not a serious problem — but let me break it down for clarity:


🔶 Message:

  • Using react-native@0.79.2 instead of recommended react-native@0.79.3

✅ What it means:

  • Your package.json currently lists React Native 0.79.2.

  • Expo SDK 53 recommends 0.79.3, which is a patch upgrade (minor bug fixes, no breaking changes).

  • It’s just a warning, not an error.


🔧 What you can do (optional):

If you want to match the exact recommended version (for smoother compatibility), run:

npx expo install react-native@0.79.3

Then re-run:

npx expo prebuild --platform android

🟢 TL;DR

  • Your current setup is okay, and you can proceed to npx expo run:android.

  • If things work fine, you can ignore the warning.

  • But for best compatibility, updating to 0.79.3 is slightly better.

Let me know if you'd like to update or move forward with the current version.

---------

I responded: Ok. I will go with npx expo run:android

I ran it ... "BUILD SUCCESSFUL in 5m 38s" [So that was quite quick!]

Then on running the app, I got the same problem with dev build app. But if I switched to Expo Go - it worked.

Using CG help, I used: adb logcat | Select-String "NoSuchMethodError"

That helped to zero in on the error:

java.lang.NoSuchMethodError: No virtual method getConverters()Lexpo/modules/kotlin/types/TypeConverterProvider; in class Lexpo/modules/kotlin/objects/ObjectDefinitionBuilder; or its super classes ...

On sharing above with CG, its summary was: "The crash is due to incompatible or mismatched Kotlin-based Expo module versions in your native Android build. Expo Go works fine because it uses its own bundled native code, but your bare/native build is broken."

I decided to explore Google search. IFIRC, that led me to the suggestion of using npx expo doctor/npx expo-doctor. I had used this in earlier projects with EAS build. That eventually led to the solution ...

TimestampTrackerWeb> npx expo-doctor
Need to install the following packages:
expo-doctor@1.13.3
Ok to proceed? (y) y

14/15 checks passed. 1 checks failed. Possible issues detected:
Use the --verbose flag to see more details about passed checks.

✖ Check that packages match versions required by installed Expo SDK
The following packages should be updated for best compatibility with the installed expo version:
  expo@53.0.9 - expected version: 53.0.10
  react-native@0.79.2 - expected version: 0.79.3
Your project may not work correctly until you install the expected versions of the packages.
Found outdated dependencies
Advice:
Use 'npx expo install --check' to review and upgrade your dependencies.

1 check failed, indicating possible issues with the project.
-----------------

Ran npx expo install --check and said y to changes.
Then ran npx expo run:android ... As part of Executing it says:
:app:checkDebugAarMetadata > Resolve files of configuration ':app:debugRuntimeClasspath' > hermes-android-0.79.3-debug.aar > 53.4 MiB/95.9 MiB downloaded
> :app:checkDebugAarMetadata > Resolve files of configuration ':app:debugRuntimeClasspath' > react-android-0.79.3-debug.aar > 115.3 MiB/213.1 MiB downloaded  
----
I think earlier version was 0.79.2 and so the new versions are being downloaded.
------

Now the app opens in dev build in Android emulator and shows one timestamp! Hmm. So patch upgraded versions of expo and react-native fixed the issue. CG had said patch upgrade part could be ignored! But npx expo-doctor insisted on fix and that solved it!

I shared the above with CG. It said:

Excellent detective work — and yes, this is a classic case where a patch version made all the difference!

You're absolutely right:

  • Earlier you had react-native@0.79.2, but Expo SDK 53 expects 0.79.3. That small patch version mismatch can easily cause runtime issues like the NoSuchMethodError you saw — especially in native/Kotlin modules, where even minor API changes (like getConverters()) can break things.

  • So when npx expo-doctor insisted and upgraded you to the correct versions (react-native@0.79.3, hermes-android, etc.), your dev build started working — because now all the versions align with what SDK 53 expects.

I did originally say the patch version warning could usually be ignored — and that’s true for JavaScript-only parts. But as you just experienced, in native code, even a patch mismatch can break ABI compatibility.


Takeaway (nicely learned by you here):

When using expo prebuild and native builds:

  • Always let expo-doctor guide version alignment.

  • Even patch differences can break the native build. Trust the tools when they say "fix this".

------------
Ravi: My view is that CG (ChatGPT) free-tier did not work out as a "vibe coding" solution for the above problem. I think based on CG usage and its responses, I need to get a sense of what I can get done well and reliably by CG free-tier and what is quite iffy even if CG's responses seem authoritative.

------------
Then the app on emulator was working in dev build but the app on phone (latest app apk installed on phone) gave error of "Unable to load script". But the standard way I used to start a dev build APK on phone was by scanning the QR code shown by Metro and then opening the link shown in browser. That led to the dev APK getting loaded or activated if it was already running. However, when I tried that I got the error of localhost refuses to connect. The issue was that even when I tried:
npx expo start --dev-client --host lan
Metro still said: 
Metro waiting on http://localhost:8081
› Scan the QR code above to open the project in a development build. Learn more: https://expo.fyi/start
...
When I scanned the QR code on my phone it attempted to open http://localhost:8081 and gets a localhost refused to connect (as probably it is looking for localhost on phone itself).

The issue was: How do I get Metro to show IP address instead of localhost?

CG suggested: Open the browser on your Android phone and type: exp://192.168.1.123:8081
That did not work. 

Eventually, qr-code-generator.com suggestion of CG worked. Specifically, I used the following procedure:
https://www.qr-code-generator.com/ -> enter exp://192.168.x.x:8081 [My IP] -> Scan code is generated to the right. Scan that scancode on phone and then open in browser. That opens the dev APK on phone and the screen is shown correctly.

-------

The following command does not show IP on Metro and so does not help to resolve dev client from phone connecting to Metro issue:
npx expo start --dev-client --host lan

----------
Checked with CG about using current icon for splash and adaptive-icon. It suggested the app.json changes which I followed. It also gave some tips for good design of splash and adaptive-icon but I did not want to get into that.
----

I asked CG to make the following changes in App.js:
1) formatInterval truncates. So 0.939 s is shown as 0s. I would like it to round instead.
2) exportTimestamps shows interval as seconds in exported file. I would like it to show days hrs mins secs like the app screen shows (using formatInterval helper function). Also, is it possible to have a header row in the exported .csv file giving labels to each column.
I copy-pasted App.js contents.

CG provided me the code snippets.

------------
I am recreating debug APK with:
npx expo run:android
BUILD SUCCESSFUL in 36s

The functionality related code changes worked. But the Android app icon was still the old one.

CG said, "React Native (via Expo prebuild) generates native Android resources like icons only once, during the first prebuild. Later changes to app.json (like adaptiveIcon) won’t automatically regenerate those files."

So, as per CG suggestions:
Deleted the native Android folder
npx expo prebuild --platform android   [Got done in few seconds]
npx expo run:android  ... Opening emulator took around 30 seconds. Then build started. "BUILD SUCCESSFUL in 3m 37s" (no downloads in build, it seems). Total time till this point was 4 min. 12 secs. Then it installed app on emulator and started it (in a few seconds). The app main screen showed correctly.

Now the app icon had changed to icon.png.

=============
Next I wanted to get to release build. Before that, I checked with CG:
For Android Kotlin app, I recall that I could run the debug APK standalone on my phone or emulator, IIRC. But for RN Android, debug APK is dependent on Metro server. Am I correct?

CG:

You're absolutely correct — and here's a clear breakdown:


✅ Android Native Kotlin App (e.g., via Android Studio)

  • Debug APKs are fully self-contained.

  • You can install and run them standalone on any device/emulator.

  • All code (including Java/Kotlin logic and resources) is compiled into the APK.

  • No need for an external server (unless your app explicitly contacts a backend).


⚠️ React Native with npx expo run:android (Dev Build)

  • The debug APK created via npx expo run:android is not fully standalone.

  • It depends on the Metro bundler running on your PC.

  • What happens:

    • The JS bundle is fetched dynamically at runtime from Metro (usually http://<your-pc-ip>:8081).

    • If Metro isn’t running, you get "Unable to load script from assets" or "localhost refused to connect" errors.

  • This is intentional to enable features like hot reloading, debugging, and live editing.


🟢 To Run Without Metro (Fully Standalone):

To run the app without Metro (like a regular Android app), you need to build a release APK or AAB:

Option 1: Debug APK with embedded JS bundle (for quick standalone testing)

You can embed the JS bundle even in a debug build like this:

npx expo export --dev npx react-native run-android --variant=release

But Expo apps need more setup for that — better go straight to the release build via EAS.

✅ Option 2: Production (Release) Build

Use EAS Build to generate a fully standalone APK or AAB:

npx expo install eas-cli npx eas build --platform android

This will:

  • Embed the JS bundle into the APK or AAB

  • Not depend on Metro or your dev machine

  • Produce an APK/AAB you can share, upload to Play Store, or sideload


I said that I wanted to do a local release build without eas. 

CG:

Step-by-Step: Local Release APK (No EAS)

🔧 1. Create a keystore (for signing the APK)

You only need to do this once.

Run this in PowerShell or terminal:

keytool -genkeypair -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000

It will prompt you for:

  • Keystore password

  • First and last name, organization, country code (you can give dummy info for local builds)

  • Key password (you can use same as keystore password)

It will generate a file: my-release-key.keystore

💡 If keytool is not found, make sure Java JDK is installed and keytool is in your PATH.


📦 2. Move the keystore to your Android project

Place the file here:

android/app/my-release-key.keystore

✍️ 3. Configure Gradle to use your keystore

Edit android/gradle.properties and add:

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

Then edit android/app/build.gradle and near the bottom of android { ... } section, add:

signingConfigs {
    release {
        storeFile file(MYAPP_UPLOAD_STORE_FILE)
        storePassword MYAPP_UPLOAD_STORE_PASSWORD
        keyAlias MYAPP_UPLOAD_KEY_ALIAS
        keyPassword MYAPP_UPLOAD_KEY_PASSWORD
    }
}
buildTypes {
    release {
        signingConfig signingConfigs.release
        minifyEnabled false
        shrinkResources false
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}

🚀 4. Build the release APK

From the root of your project, run:

cd android ./gradlew assembleRelease

On Windows PowerShell:

cd android .\gradlew.bat assembleRelease

📂 5. Find the APK

After successful build, find the release APK here:

android/app/build/outputs/apk/release/app-release.apk

You can now copy it to your phone and install it — it will run standalone, with the JS bundle included.


🟡 Optional: Test the release build locally

adb install app-release.apk

Or use a file manager to install it on your phone.

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

Did the above. Some info. about keytool command output:
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=Individual, L=Puttaparthi, ST=Andhra Pradesh, C=IN
[Storing my-release-key.keystore]
...
Ran '.\gradlew.bat assembleRelease'. ... 7% EXECUTING [52s] ... downloads have started ... "BUILD SUCCESSFUL in 10m 14s" 
app-release.apk of size 57 MB has been created in TimestampTrackerWeb\android\app\build\outputs\apk\release
-----------

On both phone and emulator, I uninstalled the Debug APK app (recommended by CG) and then installed the release app. It works on both. On phone, the app loads very fast. From user perspective, it loads as fast as my very simple Android (Kotlin) LaunchTimestamp app (only 5.7 MB APK file). That's very nice. I had wondered whether RN and Expo may result in the app loading a little slowly. [18 Jun 2025 update: Current version 1.2.0 app is a little slow to startup as compared to first release version just mentioned (version 1.0.0). I think it was after adding icon-buttons that the app screen did not show virtually instantaneously but took some noticeable milliseconds. So current version 1.2.0 app is still very fast to show main screen after launch but wee bit slower than the without icons version(s) including version 1.0.0 app.]


------------
I added C:\Users\{username}\AppData\Local\Android\Sdk\emulator to user's PATH but later removed it.

C:\Users\{username}\AppData\Local\Android\Sdk\emulator.exe -list-avds' lists my Android Virtual Devices:
Medium_Phone_API_35
I don't need the above command really. So not worth adding to user's PATH.

Added to profile:
function emu35 { & "$env:LOCALAPPDATA\Android\Sdk\emulator\emulator.exe" -avd Medium_Phone_API_35 }

To start my Android emulator:
emu35

To uninstall app (when emulator is running):
adb uninstall com.ravisiyer.timestamptracker

And to install app:
adb install android/app/build/outputs/apk/release/app-release.apk
------------

I have uploaded the release APK to a public Google Driver folder: https://drive.google.com/file/d/1e3-mUrLoaP-4Yy8lI0jR8bEaXwQZoqnv/view?usp=drive_link (57.2 MB). Interested persons can sideload the app using this APK file on their Android devices.
=============
=============
9 to 10 Jun. 2025

Next, I wanted to publish the web app.

Following instructions in: Publish websites, https://docs.expo.dev/guides/publishing-websites/

Not changing app.json as I am going with default SPA output option.

npx expo export -p web
That created a dist folder of only size 420 KB only.

npx expo serve
That executed very quickly, starting a web server. I could access the app locally on http://localhost:8081/

npx run start is for development and so starts a web server but is slower to start up than npx expo serve.
Now that npx expo serve is doing the job, perhaps I don't need to look at nginx for this simple web app and use only npx expo serve.
CG agreed.
CG suggested: 'npx expo serve --open' to open it automatically in browser. Didn't work.
.. Some trials ...
CG solution + tweaking by me that worked:
Start-Job { Start-Sleep -Seconds 1; Start-Process http://localhost:8081 } ; npx expo serve

But when I stop the server using Ctrl-C on terminal will the background job continue to run?
CG: The background job in this case just opens the browser once and exits, typically within a second or two.
...
To show background jobs:
Get-Job  (or get-job)

Id     Name            PSJobTypeName   State         HasMoreData     Location             Command
--     ----            -------------   -----         -----------     --------             -------
1      Job1            BackgroundJob   Completed     False           localhost             Start-Sleep -Seconds ...
3      Job3            BackgroundJob   Completed     False           localhost             Start-Sleep -Seconds ...

The states are completed. So does that mean that the background job process is no longer taking up system resources?

CG: The background job has finished executing. It’s no longer consuming CPU or doing any active work. However, PowerShell is still holding onto the metadata (like output, job history) in memory, until you remove the job. ... Tiny memory footprint remains (just job record info)

Me: Tiny memory footprint is no problem. Will that also go away when I close the associated Powershell terminal?
CG: Yes
-----
Added to profile:
function Start-TimestampWeb {
    Start-Job { Start-Sleep -Seconds 1; Start-Process http://localhost:8081 }
    npx expo serve
}
Set-Alias ts Start-TimestampWeb
-----
=================

Deploying to GitHub Pages

CG provided a manual option. I checked on the net and found that gh-pages npm package is popular for deploying React apps to GH Pages. Asked CG if that's OK for React Native with web apps. CG was OK with it.


npm install gh-pages --save-dev

In package.json, for my repo: https://github.com/ravisiyer/CGPTTimestamp, add after "version" entry (at root level):
 "homepage": "https://ravisiyer.github.io/CGPTTimestamp",

In package.json, add a predeploy property and a deploy property to the scripts object:
   "predeploy": "npx expo export -p web",
   "deploy": "npx gh-pages -d dist",
---   
npm run deploy

Above command output:
TimestampTrackerWeb> npm run deploy

> timestamptrackerweb@1.0.0 predeploy
> npx expo export -p web

Starting Metro Bundler
Web Bundled 170ms index.js (156 modules)

› web bundles (1):
_expo/static/js/web/index-65694d6fde41138372ae079b6cbcb749.js (408 kB)

› Files (3):
favicon.ico (14.5 kB)
index.html (1.22 kB)
metadata.json (49 B)

Exported: dist

> timestamptrackerweb@1.0.0 deploy
> npx gh-pages -d dist

Published
------

[I did not have to do the step below as it was already set in GitHub and so I just confirmed the settings were OK. Don't know how exactly that happened automatically.]
https://github.com/ravisiyer/CGPTTimestamp/settings -> Pages ->
"Build and deployment":
Source: Deploy from a branch
Branch:
Branch: gh-pages
Folder: / (root)

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

I ran into a blank page display issue for https://ravisiyer.github.io/CGPTTimestamp/ .
With CG's help, the problem seems to be lack of Expo support for GitHub Pages path handling. Create React App generated React project apparently provides support for GitHub Pages path handling and so React projects done with CRA may not face this issue. 
To be more specific, as per CG:

... there is no <base> tag and the asset paths (in dist/index.html) like:

<link rel="icon" href="/favicon.ico" />
<script src="/_expo/static/js/web/index-65694d6fde41138372ae079b6cbcb749.js" defer></script>

use absolute paths starting with /.

Why this causes a blank page on GitHub Pages:
GitHub Pages serves your app from /CGPTTimestamp/

The browser tries to load /favicon.ico and /_expo/static/... from root /, which do not exist.

This leads to 404s on critical assets, so the app cannot start and shows a blank page.

Two ways to fix:
1. Manually edit index.html in dist: [2 edits by removing / at beginning of URL]
<link rel="icon" href="favicon.ico" />
<script src="_expo/static/js/web/index-65694d6fde41138372ae079b6cbcb749.js" defer></script>
2. Add <base href="/CGPTTimestamp/"> inside <head>:
Insert this line inside <head>, preferably right after <title>:

<base href="/CGPTTimestamp/" />
and also fix the asset URLs to be relative:
<link rel="icon" href="favicon.ico" />
<script src="_expo/static/js/web/index-65694d6fde41138372ae079b6cbcb749.js" defer></script>

Me: But if I make the changes in dist/index.html manually and then run: npm run deploy
will not the predeploy step of 'npx expo export -p web' run and recreate the dist folder contents?

CG said I am right. 
One fix it suggested:

You can add this script as a postexport or inside the predeploy script chain.
1. Use a post-export script to patch dist/index.html automatically
Create a small Node.js script (or PowerShell/batch script) that modifies dist/index.html after the export completes, but before deploying with gh-pages.

You can add this script as a postexport or inside the predeploy script chain.
Example in Node.js: fix-base.js
const fs = require('fs');
const path = require('path');

const indexPath = path.join(__dirname, 'dist', 'index.html');

fs.readFile(indexPath, 'utf8', (err, data) => {
  if (err) throw err;

  let fixed = data;

  // Insert <base href="/CGPTTimestamp/"> after <title> tag
  fixed = fixed.replace(
    /(<title>.*<\/title>)/,
    `$1\n    <base href="/CGPTTimestamp/" />`
  );

  // Replace absolute paths with relative paths
  fixed = fixed.replace(/href="\/favicon.ico"/g, 'href="favicon.ico"');
  fixed = fixed.replace(/src="\/_expo/g, 'src="_expo');

  fs.writeFile(indexPath, fixed, 'utf8', (err) => {
    if (err) throw err;
    console.log('index.html fixed with base tag and relative paths.');
  });
});

Then in your package.json scripts:
"predeploy": "npx expo export -p web && node fix-base.js",
"deploy": "npx gh-pages -d dist"
---------

Me: Hmm. Quite a hassle.
Could I switch to gh-pages branch, and do the manual edit of dist/index.html and then do a git push?

CG agreed but said it will have to be done on every deploy. 

Me: fix-base.js automatically changes index.html. I have not done such stuff before in a React or React Native project or any Node environment project. So I am hesitant. I mean, what if there are bugs in fix-base.js. If I do it manually, I know exactly what I am changing. To save efforts for future npm run deploy commands, I can make a copy of the dist/index.html in the main branch somewhere (which git will include in commit of main branch). I expect dist/index.html will not change  typically and so I need to simply replace the dist/index.html generated after deploy with the saved fixed index.html.

...

Me: If I deploy to Netlify instead of GitHub Pages, will I face this same issue?

CG: If you deploy your Expo web app to Netlify, you will not face this same issue with asset paths or the base tag, because:

Netlify typically serves your site at the root domain (e.g., https://your-site.netlify.app/).

Since the app is served from the root /, the default Expo-generated asset paths (like /favicon.ico, /_expo/static/js/...) work perfectly without any path rewriting.

You don’t need a <base> tag or special configuration for subpath hosting.

---

Me: I think I will go for Netlify. I have not yet committed the gh-pages related work. So I will use Git's discard facility to discard the changes. node_modules may still have gh-pages package but that's OK. package.json will not have it and will not have the other config stuff too.
----
I discarded the uncommitted changes. So the project is back to where it was before I attempted to use GitHub Pages.
...
Me: I reverted the changes (to package.json and package-lock.json). But I am confused. Did not gh-pages push those changes to GitHub automatically? Also GitHub repo shows gh-pages branch but PC repo does not show gh-pages branch.

CG: 

✅ What gh-pages did:

When you ran:

npm run deploy

It:

  1. Ran the predeploy step: npx expo export -p web, which created/updated the dist/ directory.

  2. Then gh-pages:

    • Created a local temporary gh-pages branch behind the scenes (not visible in your local branches).

    • Pushed it directly to your GitHub repo (remote).

    • This is why you see the gh-pages branch on GitHub but not in your local Git repo.


🔍 Why you don’t see gh-pages locally:

Because gh-pages by default doesn’t create a persistent gh-pages branch in your local repo. It uses a detached branch state temporarily, commits the contents of dist, pushes to the remote, then removes the branch locally.

--------

The CG explanation seems to make sense. ... 

Me: Hmm. So as gh-pages branch is only on GitHub now, as a temp fix, I can directly change the index.html on GitHub to have relative paths instead of absolute paths. My local project will not know anything about it. Further, the gh-pages npm package is removed from package.json and so the gh-pages branch on GitHub will not get changed by gh-pages.

CG was OK with it and said, "Save changes by committing directly to the gh-pages branch."

Me: But if I commit to gh-pages branch on GitHub, will it impact future commits I make on main branch on local PC and which I push to GitHub

CG:

No, committing directly to the gh-pages branch on GitHub will not impact your local main branch or its future commitsas long as you don’t merge gh-pages into main, or track gh-pages locally. Here's how it works:


🔒 GitHub Branches Are Independent by Default

When you:

  • Make a commit to the gh-pages branch (directly on GitHub), and

  • Push future commits from your local main branch (from your PC),

👉 those are completely separate operations.

Unless you explicitly fetch/merge gh-pages into main (which you won’t), there’s no interference.

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

I tried option 1 first (without base statement). That did not work. Then I tried option 2. But that too showed blank page. I dug deeper by looking at browser console. It showed this error: 

index-65694d6fde41138372ae079b6cbcb749.js:1 
            
            
           Failed to load resource: the server responded with a status of 404 ()
-------
The link address is: https://ravisiyer.github.io/CGPTTimestamp/_expo/static/js/web/index-65694d6fde41138372ae079b6cbcb749.js

But that matches the filename and path in the GH repo. The file can be seen at: https://github.com/ravisiyer/CGPTTimestamp/blob/gh-pages/_expo/static/js/web/index-65694d6fde41138372ae079b6cbcb749.js

Its path is shown as: _expo/static/js/web/index-65694d6fde41138372ae079b6cbcb749.js

https://ravisiyer.github.io/CGPTTimestamp/favicon.ico shows correctly. Only above fails!

Directly trying to open https://ravisiyer.github.io/CGPTTimestamp/_expo/static/js/web/index-65694d6fde41138372ae079b6cbcb749.js gives:
404
File not found

The site configured at this address does not contain the requested file.

If this is your site, make sure that the filename case matches the URL as well as any file permissions.
For root URLs (like http://example.com/) you must provide an index.html file.

Read the full documentation for more information about using GitHub Pages.

GitHub Status — @githubstatus

----

CG:

The issue is subtle but common when using GitHub Pages with Expo web builds.


✅ Problem Summary

Even though the file _expo/static/js/web/index-65694d6fde41138372ae079b6cbcb749.js exists in the GitHub repo (gh-pages branch), the browser shows:

404 - File not found

when you try to load:

https://ravisiyer.github.io/CGPTTimestamp/_expo/static/js/web/index-65694d6fde41138372ae079b6cbcb749.js

🔍 Root Cause: GitHub Pages Ignores _ (underscore) Folders

GitHub Pages by default does not serve content from folders that start with an underscore (like _expo/) — this is a known GitHub Pages behavior because Jekyll (which powers GitHub Pages) treats underscore folders as special/private.


✅ Fix Options

🔧 Option 1: Disable Jekyll for GitHub Pages

Create an empty file named .nojekyll in the root of the gh-pages branch.

This will instruct GitHub Pages to bypass Jekyll, allowing _expo/ and other underscored directories to be served normally.

Steps:

  1. Go to your GitHub repo → gh-pages branch.

  2. Add a new file in the root of the branch:

    • Name: .nojekyll

    • Content: (leave it empty)

  3. Commit the file.

  4. Wait a minute and refresh:
    https://ravisiyer.github.io/CGPTTimestamp/

That should now load the JS file correctly.

--------

Me: Adding .nojekyll file fixed it. Now the app main page displays correctly!
...
Now, for experience's sake, I will try to continue to use GitHub Pages to host the web app of this project. I mean, GH pages is well known and free and seems to have lot of capacity. Being able to deploy a React Native + Expo web app to GH pages may be a useful skill to cement. So I will go back to using gh-pages npm package in the project. 
---------

Ran: npm install gh-pages --save-dev

✅ What Happens:

  • If gh-pages is already present in node_modules/ (from a past install), but not listed in package.json, then:

    • npm install gh-pages --save-dev will simply:

      • Ensure it's listed under "devDependencies" in package.json

      • Reinstall or verify the correct version is present

      • Avoid duplicates (thanks to npm’s deduping logic)

---
Fixed some minor UI issues with CG help including some retries:
  • White bar appearing on top of header in web on dark mode
  • On Web max-width 800px and if window width is greater main content to be centered (this needed some retries).
  • Gap between last entry and first bottom button being larger than usual (seen when having scrolled to bottom)
I am putting this fix separately as the related content is longish:
Me: I am facing a strange issue. padding seems to have an impact only on Android app but paddingTop and paddingBottom impact both web and Android. There was no gap after the bottom button on web and it was flush with the window bottom edge. The following code gives me a temporary fix. When I have time later, I will debug the web display at CSS level and see why padding is not impacting web display.

    container: {
      flex: 1,
      padding: 20,
      paddingTop: 10,     // overrides only the top padding
      paddingBottom: 10,
      // marginTop: 30,
      backgroundColor: isDark ? '#000' : '#fff',
    },

CG:

You're exactly right in your observation — and your workaround is practical and sensible. Here's a bit of clarity on why you're seeing this behavior:


✅ Why padding behaves differently on Web vs Android

In React Native Web, the padding prop on a View is translated into CSS. However:

  • On Android (native), padding behaves as expected — it's part of the layout engine.

  • On Web, the computed CSS rules might be influenced by:

    • The browser’s default CSS reset (or lack thereof).

    • The element's height being constrained by flex layout, causing some parts of padding (especially bottom) to be collapsed or ignored unless explicitly set.

That’s why:

padding: 20,

on its own doesn’t always guarantee top/bottom spacing unless overridden like:

paddingTop: 10, paddingBottom: 10,

✅ Why paddingTop and paddingBottom work on both platforms

Setting paddingTop and paddingBottom explicitly ensures React Native Web emits those as:

padding-top: 10px; padding-bottom: 10px;

So they bypass any inconsistencies in default browser rendering or flex shrink/stretch.


🛠️ Your Fix (Recap)

container: { flex: 1, padding: 20, paddingTop: 10, // explicitly override top paddingBottom: 10, // explicitly ensure space at bottom backgroundColor: isDark ? '#000' : '#fff', },

✔ This is a perfectly reasonable approach and makes the layout reliable on both Android and Web.


✅ When you debug later

When you revisit with Chrome DevTools (web), look at:

  • The computed padding, margin, and height on the container.

  • How SafeAreaView and StatusBar are rendering on Web.

  • Whether default body, html, or root div styles are interfering.


I have saved an empty .nojekyll file in dist-fix folder at project root. Earlier I had tried using copy of fixed index.html too but that did not work as the script filename changed across expo export web builds.

I deployed to GH-Pages by executing individual commands instead of 'npm run deploy':
  1. npx expo export -p web
    1. Created dist files but index.html had absolute path problem
  2. cp dist-fix\.nojekyll dist\
  3. Edit index.html to 
    1. Remove starting / from link and script elements
    2. Add after title element in head: <base href="/CGPTTimestamp/" />
  4. npx gh-pages -d dist
The above procedure works. Later as I get more comfortable with this fix, I will explore automating it with a script after which I can use 'npm run deploy'.

CG found above to be OK. Additionally it said:

🚀 Future Automation Tip

You could write a simple post-export script (fix-html.js) like:

// fix-html.js const fs = require('fs'); const path = './dist/index.html'; let html = fs.readFileSync(path, 'utf8'); html = html.replace(/(src|href)="\//g, '$1="'); // remove leading / html = html.replace(/<title>(.*?)<\/title>/, `<title>$1</title><base href="/CGPTTimestamp/">`); fs.writeFileSync(path, html);

Then in package.json:

"scripts": { "export-web": "expo export -p web", "fix-html": "node fix-html.js", "deploy": "npm run export-web && cp dist-fix/.nojekyll dist/ && npm run fix-html
         && gh-pages -d dist" }

And deploy via:

npm run deploy

This is an alternative to earlier suggestion it had made in this regard.

----------------
One odd issue with Expo Go app (Android) is that bottom button initially is shown on top of phone navigation buttons (whatever that is called) but after some UI action like switching to another app and then coming back to this app, the bottom button is shown at same level as navigation buttons but the button is not pressable as the navigation buttons get pressed. Reloading the app fixes the issue. In Android release app, this issue is not seen.

CG:

Thanks for sharing that — what you're observing with the Expo Go app (Android) is a known UI quirk related to safe area insets and translucent system UI (status bar and navigation bar) handling. Here's a breakdown:


🧠 What’s Happening:

  • Expo Go runs your app in a shell that doesn’t fully handle safe areas like a production/release build does.

  • The navigation bar (soft buttons) at the bottom can overlap content if the view isn't padded correctly.

  • After resuming from background, layout recalculation might fail to reapply insets properly, leading to:

    • Buttons visually appearing in the right place [Ravi????]

    • But presses being intercepted by the system navigation


✅ Why the Issue Doesn’t Occur in Release App:

  • When you build the app (expo build or eas build) and run the release version, React Native and system APIs (via react-native-safe-area-context) handle the insets properly.

  • So in the release APK/AAB, SafeAreaView or appropriate padding/margin ensures interactive UI stays above system bars.

...
Me: Can the app detect that it is running in Expo Go and so have a conditional behaviour to overcome this issue. This way the production app is not impacted.

CG: Yes, you can detect whether your app is running in Expo Go, and conditionally apply fixes for layout or other quirks that affect only the Expo Go environment — without impacting your production app.

How to Detect Expo Go at Runtime

Use the expo-constants package (which comes pre-installed in managed Expo projects):

[Ravi: First suggested solution using Constants.appOwnership gave a warning that Constants.appOwnership is deprecated. Following solution did not have any such issue and it worked with a tweak of 40 instead of suggested 20.]

import Constants from 'expo-constants'; const isExpoGo = Constants.executionEnvironment === 'storeClient'; ... container: { flex: 1, padding: 20, // Has no effect on top/bottom on web paddingTop: 10, paddingBottom: isExpoGo ? 40 : 10, // Extra padding only in Expo Go backgroundColor: isDark ? '#000' : '#fff', },
------------

Next, I explored automation for the fix for publishing to GitHub pages. 
In dist-fix directory (which is not in .gitignore and so is part of GH repo), I created a fix-index-html.js file whose contents are:
const fs = require('fs');
const path = './dist/index.html';

let html = fs.readFileSync(path, 'utf8');
html = html.replace(/(src|href)="\//g, '$1="'); // remove leading /
html = html.replace(/<title>(.*?)<\/title>/, `<title>$1</title><base href="/CGPTTimestamp/">`);
fs.writeFileSync(path, html);
----
Then I tried following in package.json:
"scripts": {
    "predeploy": "npx expo export -p web && Copy-Item dist-fix/.nojekyll dist/ && node ./dist-fix/fix-index-html.js",
    "deploy": "npx gh-pages -d dist",
-----
Running 'npm run predeploy' gave the error:
'Copy-Item' is not recognized as an internal or external command, operable program or batch file.
-----
CG said that the error "means that Copy-Item is a PowerShell-specific command, and you're running this script in a context (like cmd.exe or npm run) that uses CMD, not PowerShell."

CG responses have now become quite slow for me (on free tier). Earlier, I would typically get the full response at one go. Now I am shown the response in parts with signficant time breaks between the response parts. So after I saw the above part of CG response and there was a delay in CG showing further part of the response, I tried something out on my own.  [Later update: I created a new chat. In the new chat, the responses seem to be faster. Hmm. So perhaps the long length of the old chat contributed to slower responses from ChatGPT. ... 11 Jun 2025 Update: Seems like ChatGPT had some outage issues yesterday and that may have been the reason for the issues mentioned above. Ref: (I saw the article on 10 Jun 2025 or 11 Jun 2025 early hours; now the article has "Updated - June 10, 2025 07:26 pm IST - New Delhi") ChatGPT faces global outage; India, U.S. most affected, https://www.thehindu.com/sci-tech/technology/chatgpt-faces-global-outage-india-us-most-affected/article69678950.ece .]


The key point was the CMD point. That led to this solution which works on my Win 11 PC:
In package.json:
  "scripts": {
    "predeploy": "npx expo export -p web && copy dist-fix\\.nojekyll dist\\ && node ./dist-fix/fix-index-html.js",
    "deploy": "npx gh-pages -d dist",
----
Meanwhile the rest of CG's response was shown in the browser. The key point was that such CMD or even Powershell approaches ('&& powershell -Command \"Copy-Item dist-fix/.nojekyll dist/\"') will not work on MacOS/Linux (Ravi: unless they have powershell set up, in case of Powershell approach).

So I am now going for the cross-platform solution 
...
After a retry, got this working solution:
dist-fix\fix-expo-gh.js contents:
const fs = require('fs');
const path = require('path');

// Config
const repoName = '/CGPTTimestamp';
const distDir = path.join(__dirname, '..', 'dist');
const fixDir = path.join(__dirname);
const nojekyllSrc = path.join(fixDir, '.nojekyll');
const nojekyllDest = path.join(distDir, '.nojekyll');
const indexHtmlPath = path.join(distDir, 'index.html');

// 1. Copy .nojekyll
fs.copyFileSync(nojekyllSrc, nojekyllDest);
console.log('✓ Copied .nojekyll');

// 2. Read and modify index.html
let html = fs.readFileSync(indexHtmlPath, 'utf-8');

// Add <base href="..."> after <title>
html = html.replace(/<title>(.*?)<\/title>/, (match, titleText) => {
  return `<title>${titleText}</title>\n  <base href="/${repoName}/">`;
});

// Remove leading / in href/src
html = html.replace(/(href|src)="\/(.*?)"/g, '$1="$2"');

fs.writeFileSync(indexHtmlPath, html, 'utf-8');
console.log('✓ Patched index.html');
----

package.json entries change:
  "scripts": {
    "predeploy": "npx expo export -p web && node ./dist-fix/fix-expo-gh.js",
    "deploy": "npx gh-pages -d dist",
...
-----

With the above changes, 'npm run deploy' does the expo web build, fixes the expo GitHub Pages issue, and then using gh-pages publishes to GitHub Pages. Within a few minutes after publish, the site: https://ravisiyer.github.io/CGPTTimestamp/ is live with the latest published code.
-----

Next, with CG's help I changed the app to have one row with three buttons: 'Export to File', 'Clear All' and a new 'Info' button which currently does not have an OnPress handler. Earlier on, I myself had moved 'Add Timestamp' to bottom (as that is more convenient for one-hand usage on phone, I think).

I then added an 'Add TS' button to the top row as well. Now the top row has four buttons with shortened labels so that they display OK on Android phone screen width.

I also wanted to round the buttons. CG said, " To make the buttons rounded, you'll need to use TouchableOpacity or Pressable with custom View and Text styling, since the default Button component in React Native doesn’t support border radius or much custom styling." It then provided a solution but I wanted to keep the code simple and so did not try that out.
--------

Noted that 'npx gh-pages -d dist' results in a commit on gh-pages branch of GH repo only if dist contents on PC are different from that on last commit on gh-pages branch of GH repo. If they are the same, while gh-pages execution shows a "Published" message, there is no commit on gh-pages branch of GH repo, and so a new deploy is NOT done.
----

Me (to ChatGPT): Now, for the Info button, I want an OnPress handler that does the following:
1) Shows a modal which handles dark/light mode properly.
2) Within the modal, the following info. should be shown:
2a) 
This is a very simple launch and one-touch-add timestamp recorder app with no text associated with the timestamp. It automatically creates a timestamp when the app is launched.

Clear All button: Clears all timestamps.
Add button: Adds current date & time as a timestamp and shows the interval from last timestamp.
App author: Ravi S. Iyer with ChatGPT's assistance
App date: 10 Jun. 2025

App blog post: [Link to be given here: Using ChatGPT to write React Native and Expo Timestamp app (web and mobile), https://raviswdev.blogspot.com/2025/06/using-chatgpt-to-write-react-native.html ]
2b) A 'Dismiss' button should be provided at the bottom of the modal which will close the modal.
-------

CG took too long without any response. Refreshed page and chose Retry. Once again it seems to be taking long with no response.
I am now trying out Gemini with same message. Gemini responded within seconds showing lot of code suggestions. 
That needed a minor tweak for blog post url. Other that that, it is working well including dark/light mode support and opening blog post from Info modal! Have modified blog post and app info to say that assistance was taken from ChatGPT and Gemini.
Commited: Modal added using Gemini; Ack both CGPT and Gem
Published to web using 'npm run deploy'.
Within few minutes, the GH Pages site was updated with the new code and Info modal worked correctly there too.
-----
Next I uploaded my current, working App.js into Gemini. Then I asked:
I would like the following change:
Currently the time interval is shown and exported as days, hrs, mins, secs.
I would like it to be shown and exported as days, hrs, mins, secs, millisecs
-----
Gemini's response had an error! I am giving the details here, just as documentation for how Gemini can trip up on a calculation related matter.

Gemini's suggested code for formatInterval method:
const formatInterval = (totalMilliseconds) => { // Changed parameter name to reflect milliseconds
  if (totalMilliseconds === null || isNaN(totalMilliseconds)) return ''; // Handle potential null/NaN for consistency

  const total = Math.round(totalMilliseconds / 1000); // Total seconds for larger units
  const days = Math.floor(total / 86400);
  const hrs = Math.floor((total % 86400) / 3600);
  const mins = Math.floor((total % 3600) / 60);
  const secs = total % 60;
  const millisecs = Math.round(totalMilliseconds % 1000); // Remaining milliseconds

  let result = '';
  if (days > 0) result += `${days}d `;
  if (hrs > 0 || days > 0) result += `${hrs}h `;
  if (mins > 0 || hrs > 0 || days > 0) result += `${mins}m `;
  result += `${secs}s `; // Add a space after seconds for consistency
  result += `${millisecs}ms`; // Add milliseconds

  return result.trim();
};
---end Gemini code snippet---

On having a look at the code, I felt there was something wrong in the calculation due to 'const total = Math.round(totalMilliseconds / 1000);'  ...

Me (to Gemini): In the following code:
  const total = Math.round(totalMilliseconds / 1000); // Total seconds for larger units
  const days = Math.floor(total / 86400);
  const hrs = Math.floor((total % 86400) / 3600);
  const mins = Math.floor((total % 3600) / 60);
  const secs = total % 60;
  const millisecs = Math.round(totalMilliseconds % 1000); // Remaining milliseconds
--end code snippet--------

why do we need to round secs now? Earlier it was needed as we were not provide millseconds.
----

Gemini gave a long response. A key part of it was:

Why secs is "Rounded" (Indirectly):

  • When you calculate secs = total % 60;, you are taking the remainder of the total (which is already a rounded whole number of seconds) divided by 60.
  • Since total is a whole number, total % 60 will always result in a whole number.
  • Example:
    • If totalMilliseconds was 60500 (60.5 seconds), then total becomes Math.round(60.5) which is 61.
    • Then secs = 61 % 60 which is 1.
    • The millisecs will be Math.round(60500 % 1000) which is 500.
    • So, 60.5 seconds is correctly represented as "1m 1s 500ms".
----- end Gemini response extract ----

Me: The example you gave seems to be wrong. 60500 milliseconds should be 1 m 0s 500 ms and NOT 1m 1s 500ms. Or did I get something wrong?

Gemini: You are absolutely, 100% correct! My apologies. That was a significant error in my example, and I appreciate you catching it.

Let's re-evaluate the example with totalMilliseconds = 60500 (60.5 seconds) and correct the math:

  1. total = Math.round(totalMilliseconds / 1000);

    • total = Math.round(60500 / 1000);
    • total = Math.round(60.5);
    • total = 61;
    • This is the source of the error in my previous explanation. The Math.round() here was the culprit. If total becomes 61, then the secs calculation becomes incorrect.
  2. The Correct Approach for formatInterval

    For days, hrs, mins, and secs, we should be using the floor of the division by 1000 to get the integer number of seconds, not rounding. Rounding can introduce an extra second too early, as you correctly pointed out.

    Let's refine the formatInterval function:

[The code Gemini gave:]

const formatInterval = (totalMilliseconds) => {
  if (totalMilliseconds === null || isNaN(totalMilliseconds)) return '';

  // Calculate total seconds by flooring, not rounding, to correctly extract larger units
  const totalSeconds = Math.floor(totalMilliseconds / 1000);

  const days = Math.floor(totalSeconds / 86400);
  const hrs = Math.floor((totalSeconds % 86400) / 3600);
  const mins = Math.floor((totalSeconds % 3600) / 60);
  const secs = totalSeconds % 60; // This is now correct based on totalSeconds

  // Milliseconds are the remainder after extracting full seconds
  const millisecs = Math.round(totalMilliseconds % 1000);

  let result = '';
  if (days > 0) result += `${days}d `;
  if (hrs > 0 || days > 0) result += `${hrs}h `;
  if (mins > 0 || hrs > 0 || days > 0) result += `${mins}m `;
  result += `${secs}s `;
  result += `${millisecs}ms`;

  return result.trim();
};
------end Gemini code and response ----

The above code looks OK. I will test it along with other code changes Gemini had provided earlier on. I am quite surprised that not only did Gemini make a mistake in the first code it provided, but when I asked about it specifically, even then it did not catch it and instead gave a wrong example, trying to convey the impression that it was right. Fortunately, after I pointed out the error in the example, it got its error and seems to have fixed it.

Improved readability in above code with Gem (Gemini) help. 

Got Gem to provide automated test driver to test formatInterval (moved to util.mjs file)
With Gem's help came up with a way to provide console logging (for testing) in formatInterval based on environment variable test. The test driver can be invoked after setting env variable on Powershell command line - so no .env file issue.
// # To run your tests with the formatInterval logs ENABLED:
// $env:ENABLE_FORMAT_INTERVAL_LOGS="true"; node ./formatInterval.test.mjs

// # To run your tests with the formatInterval logs DISABLED (default if variable is not set or not 'true'):
// node ./formatInterval.test.mjs

Used .mjs extension for test driver and util files to avoid warnings on running test driver from node

Current formatInterval code (in util.mjs) is given below:
...
// This constant is evaluated only once when the module loads.
// Logs will appear if NOT in production AND if ENABLE_FORMAT_INTERVAL_LOGS environment variable is 'true'.
const TEST_FORMAT_INTERVAL = process.env.NODE_ENV !== 'production' &&
                              process.env.ENABLE_FORMAT_INTERVAL_LOGS === 'true';
...
export const formatInterval = (totalMilliseconds) => {
  // Conditional logging: only log if not in production environment
  if (TEST_FORMAT_INTERVAL) {
    console.log(`\n--- formatInterval called with totalMilliseconds: ${totalMilliseconds} ---`);
  }

  if (totalMilliseconds === null || isNaN(totalMilliseconds)) {
    if (TEST_FORMAT_INTERVAL) {
      console.log("Input is null or NaN, returning empty string.");
    }
    return '';
  }

  const MS_PER_SECOND = 1000;
  const MS_PER_MINUTE = 60 * MS_PER_SECOND;
  const MS_PER_HOUR = 60 * MS_PER_MINUTE;
  const MS_PER_DAY = 24 * MS_PER_HOUR;

  if (TEST_FORMAT_INTERVAL) {
    console.log(`Constants: MS_PER_SECOND=${MS_PER_SECOND}, MS_PER_MINUTE=${MS_PER_MINUTE}, MS_PER_HOUR=${MS_PER_HOUR}, MS_PER_DAY=${MS_PER_DAY}`);
  }

  const days = Math.floor(totalMilliseconds / MS_PER_DAY);
  let remainderMs = totalMilliseconds % MS_PER_DAY;
  if (TEST_FORMAT_INTERVAL) {
    console.log(`Days: ${days}, Remainder after days: ${remainderMs}ms`);
  }

  const hrs = Math.floor(remainderMs / MS_PER_HOUR);
  remainderMs %= MS_PER_HOUR;
  if (TEST_FORMAT_INTERVAL) {
    console.log(`Hours: ${hrs}, Remainder after hours: ${remainderMs}ms`);
  }

  const mins = Math.floor(remainderMs / MS_PER_MINUTE);
  remainderMs %= MS_PER_MINUTE;
  if (TEST_FORMAT_INTERVAL) {
    console.log(`Minutes: ${mins}, Remainder after minutes: ${remainderMs}ms`);
  }

  const secs = Math.floor(remainderMs / MS_PER_SECOND);
  const millisecs = remainderMs % MS_PER_SECOND;
  if (TEST_FORMAT_INTERVAL) {
    console.log(`Seconds: ${secs}, Milliseconds: ${millisecs}`);
  }

  let result = '';
  if (days > 0) result += `${days}d `;
  if (hrs > 0 || days > 0) result += `${hrs}h `;
  if (mins > 0 || hrs > 0 || days > 0) result += `${mins}m `;
  result += `${secs}s `;
  result += `${millisecs}ms`; // Always add milliseconds

  const finalResult = result.trim();
  if (TEST_FORMAT_INTERVAL) {
    console.log(`Final formatted string: "${finalResult}"`);
    console.log(`--- End formatInterval ---`);
  }
  return finalResult;
};

=============
=============
11 Jun. 2025

Me to Gem: If I recall correctly, C++ had a facility to use preprocessor directives to include debug code only in development builds and not production builds. In our JS code, we have:

  if (TEST_FORMAT_INTERVAL) {

    console.log(`\n--- formatInterval called with totalMilliseconds: ${totalMilliseconds} ---`);

  }

------

While it does the job of not flooding the console unless we set the debug environment variable, it has a minor disadvantage of the if check running in production build which is unnecessary. Does JS have a preprocessor directive to include such debug statements only in development builds but not in production builds?

Gem: 

That's a fantastic question, and it shows you're thinking deeply about build optimizations! You're absolutely right that in C++, preprocessor directives like #ifdef DEBUG ... #endif allow the compiler to completely strip out debug code from release builds, so there's no runtime overhead.

In native JavaScript, there isn't a direct equivalent to a C++ preprocessor. JavaScript is interpreted or just-in-time compiled at runtime, so there's no compile-time #ifdef equivalent built into the language itself.

However, the good news is that the solution we implemented (using process.env.NODE_ENV and process.env.ENABLE_FORMAT_INTERVAL_LOGS) does achieve this exact "preprocessor-like" behavior in production builds, thanks to modern JavaScript bundlers!

Here's how it works with tools like Metro (for React Native/Expo), Webpack, Rollup, etc.:

  1. Environment Variable Replacement: When you perform a production build (e.g., eas build, expo export, or webpack --mode production), your bundler (Metro in your case) has a build-time step where it reads environment variables. It will typically replace all occurrences of process.env.NODE_ENV with its actual string value, which is 'production' in a production build.

  2. Constant Folding / Dead Code Elimination (Tree-Shaking): Let's trace your DEBUG_FORMAT_INTERVAL constant in a production build:

    const DEBUG_FORMAT_INTERVAL = process.env.NODE_ENV !== 'production' &&                              process.env.ENABLE_FORMAT_INTERVAL_LOGS === 'true';

    In a production build, this effectively becomes:

    const DEBUG_FORMAT_INTERVAL = 'production' !== 'production' && process.env.ENABLE_FORMAT_INTERVAL_LOGS === 'true';

    Which simplifies to:

    const DEBUG_FORMAT_INTERVAL = false && process.env.ENABLE_FORMAT_INTERVAL_LOGS === 'true';

    And finally, at build time, it's known that:

    const DEBUG_FORMAT_INTERVAL = false;

    Now, every if statement you wrote:

    if (DEBUG_FORMAT_INTERVAL) {
      console.log(`...`);
    }
    

    becomes, during the build process:

    if (false) {
      console.log(`...`);
    }
    

    Since the condition false is constant and known at build time, the bundler's minifier and tree-shaking (dead code elimination) optimizations will recognize that the code inside the if (false) block is unreachable. It will then completely remove that code from your final production JavaScript bundle.

So, to summarize:

  • In Development (e.g., expo start or node for tests with ENABLE_FORMAT_INTERVAL_LOGS=true): The if check runs, and console.log statements execute, giving you valuable debugging output.
  • In Production Builds: The if check and all the console.log statements within those if blocks are completely stripped out of your final app bundle by Metro's optimization pipeline. There will be no runtime check or unnecessary code.

This is the standard and highly effective way to achieve C++ preprocessor-like conditional compilation in JavaScript, leveraging the power of modern build tools. You've implemented it correctly!

Me: Wow! Great to see this level of optimization in modern JavaScript. And wow for your super explanation too.

-------

Using Gem's help, I added milliseconds to timestamp display. This helps to explain the Interval milliseconds.
Next I wanted date to be shown with month as three letters (like Jun). I also wanted to use the locale of the user so that for English India ('en-IN') locale we have dd MMM yyyy (11 Jun 2025) whereas for English United States ('en-US') locale we have Jun 11, 2025. This turned out to be non-trivial. The eventual solution was not that complicated but as I dug into some aspects of optional parameters for toLocaleString() function and found some quirks in that implementation, it sucked up a lot of time. I will give the learnings here in short:
  1. The initial suggestion of Gem to simply omit the optional locale parameter (first parameter) did not work. The month part of the date continued to be showed as digits. The related code :  
    1. // Omitting the first argument (locale) will make it use the user's system locale
    2.   const dateTimePart = current.toLocaleString(undefined, { // 'undefined' explicitly passes nothing, or just remove 'undefined,'
    3.     year: 'numeric',
    4.     month: 'short',    // e.g., Jun (or "अक्तू" for Hindi, "июня" for Russian, etc., depending on locale)
    5.     day: 'numeric',
    6. ...
  2. The next suggestion from Gem was to use Expo localization. But before I tried that, I explored some variations of earlier solution as I wanted to see if I could avoid using Expo localization.
  3. After some digging up, I spotted one issue I had which was that my Chrome browser language was English (United States) though my Windows OS setting for date format was India. Changed Chrome browser language to English (India). Then I ran the following in TryIt box of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString :
    1.     const dateTimePart = (new Date()).toLocaleString( {
    2.       year: 'numeric',
    3.       month: 'short',    // e.g., Jun (or "अक्तू" for Hindi, "июня" for Russian, etc., depending on locale)
    4.       day: 'numeric',
    5. ...
    6.     });
    7. console.log(dateTimePart);

    8. Output it gives is:
    9.  "11/06/2025, 20:48:55"

    10. The format seems to match India but month is shown as digits and not as letters.
  4. If I used a specific locale parameter like 'en-IN' then .toLocaleString() gave month in letters!
  5. To get the user locale in javascript running in browser as against React Native and Expo, navigator.language gives the locale. Using that gave month in letters. For .toLocaleString() or toLocaleDateString(), locale seems to be necessary at least in some scenarios for the Options parameter to be fully effective, even in standard JavaScript without React or React Native or Expo.
  6. [Gem (slightly edited):] You cannot use navigator.language directly in your React Native app.
    1. navigator.language is a Web API: It's a property of the Navigator interface, which is part of the Web APIs designed for browser environments. It gives you information about the user agent (the browser).
    2. React Native is not a browser: A React Native app runs its JavaScript code in a JavaScript runtime (like JavaScriptCore on iOS/Android or Hermes for newer Expo/RN versions) that is embedded within a native mobile application. This environment does not include browser-specific objects like window, document, or navigator.
    3. This is precisely why libraries like expo-localization (which we discussed) exist. They provide a bridge to access native device functionalities (like the device's current locale) that aren't exposed through standard Web APIs.
    4. So, for your React Native app, Localization.locale from expo-localization is indeed the correct and necessary equivalent to navigator.language.
  7. Localization.locale was deprecated and so I had to use: Localization.getLocales()[0].languageTag. Passing that to toLocaleString() function fixed the issue. Example output:
    1. 11 Jun 2025, 10:05:39 pm (946ms)
    2. Interval: 4m 28s 294ms
    3. 11 Jun 2025, 10:01:11 pm (652ms)
    4. Interval: 4m 9s 95ms
  8. Got Gem to explain the difference between Date().toLocaleString() and (new Date()).toLocaleString(). Key points about it are given below:
    1. Code:
      1. console.log(Date().toLocaleString());
      2. console.log((new Date()).toLocaleString());
      3. console.log((new Date()).toString());
    2. Output (a later run due to which time is a little later than in CG explanations in later points):
      1.  "Wed Jun 11 2025 22:19:15 GMT+0530 (India Standard Time)"
      2.  "11/06/2025, 22:19:15"
      3.  "Wed Jun 11 2025 22:19:15 GMT+0530 (India Standard Time)" 
    3. When you call Date() without the new keyword, it behaves as a function that simply returns a string representation of the current date and time. This string is essentially the same as what (new Date()).toString() would produce.
      1. In the code:
        1. console.log(Date().toLocaleString());
      2. Date() executes first, returning the string: "Wed Jun 11 2025 22:08:41 GMT+0530 (India Standard Time)"
      3. Then, .toLocaleString() is called on that string.
      4. The toLocaleString() method for strings (String.prototype.toLocaleString()) simply returns the string itself, as strings don't have locale-specific formatting logic beyond what's already in them.
    4. When you use the new keyword with Date() (i.e., new Date()), you are calling Date as a constructor. This creates and returns a new Date object instance. This object is a special JavaScript object type designed to hold date and time information.
      1. In the code:
        1. console.log((new Date()).toLocaleString());
      2. (new Date()) executes first, creating a Date object (e.g., an object representing Wed Jun 11 2025 22:08:41 GMT+0530 (India Standard Time) internally).
      3. Then, .toLocaleString() is called on that Date object.
      4. The toLocaleString() method for Date objects (Date.prototype.toLocaleString()) is specifically designed to format the date and time information stored in the object into a human-readable string, taking into account locales and options.
      5. This is why your second output is the correctly formatted date and time string ("11/06/2025, 22:08:41"), based on the runtime's default locale.
    5. In essence: You're calling toLocaleString() on two fundamentally different types of JavaScript values: a string in the first case, and a Date object in the second. Each type has its own toLocaleString() method, which behaves differently.
=====================
=====================
Late Hrs 11 Jun to 12 Jun 2025

Me: This continues the immediately previous chat which has become very long and so I am creating a new chat.
In the following code, I want these changes:

a) From renderItem method's code, create a helper method called formattedTimestamp which takes in a date parameter and returns the formatted Timestamp with millisecs. Modify renderItem to use that helper method.

b) Modify exportTimestamps method to use the same helper method formattedTimestamp instead of its current code of: ${new Date(t).toLocaleString()}
[I copy-pasted the code]

Gem gave me the modified code.

Me: It works!

Will Metro/webpack bundlers optimize the 'const userLocale = Localization.getLocales()[0].languageTag;' part of the function:
  const formattedTimestamp = (date) => {
    const userLocale = Localization.getLocales()[0].languageTag;
----

Gem gave a long answer. Key part:
In summary:

The line const userLocale = Localization.getLocales()[0].languageTag; will be executed every time formattedTimestamp is called. However, this is generally not a performance bottleneck because:

The operation itself is fast.
The bundlers will minify the code, but not remove the dynamic runtime call.
Your current implementation is good for maintainability and readability, and the performance impact is negligible for this specific operation.
-------

Committed changes.
npm run deploy.
The GH Pages site has been updated and seems to work OK.
=========

Me (to Gem): I now want to build an Android release APK. For my previous release APK, I had done an 'npx expo prebuild'. Then I had set up the gradle config, APK signing etc. and finally ran, '.\gradlew.bat assembleRelease' from android folder in project. 
I installed expo-localization package after my previous release APK build. That seems to have also added an expo-localization plugin in app.json.
For creating the release APK afresh, can I straight away run '.\gradlew.bat assembleRelease' from android folder in project? Or do I have to go through the entire procedure again starting with 'npx expo prebuild'?

Gem:

This is an excellent question and gets to the heart of how Expo's "prebuild" (now often referred to as Continuous Native Generation or CNG) works in conjunction with a bare React Native workflow.

The short answer is: You most likely need to run npx expo prebuild again.

Here's why and the breakdown:

  1. Understanding npx expo prebuild:

    • When you run npx expo prebuild, Expo takes your app.json (or app.config.js) configuration and your installed expo-* packages, and then generates (or regenerates) the native android and ios directories.
    • This process involves applying "config plugins" that various Expo modules use. These plugins modify native project files (like AndroidManifest.xml for Android, or Info.plist and Gradle/CocoaPods files) to correctly link and configure the native code of the Expo modules.
  2. Why expo-localization and its plugin matter:

    • You installed expo-localization after your previous npx expo prebuild.
    • The documentation explicitly states: "You can configure expo-localization using its built-in config plugin if you use config plugins in your project (EAS Build or npx expo run:[android|ios]). The plugin allows you to configure various properties that cannot be set at runtime and require building a new app binary to take effect."
    • This means expo-localization requires specific native configurations that are handled by its plugin. For these changes to be incorporated into your Android project, the native files in the android directory need to be updated.
  3. The role of .gradlew.bat assembleRelease:

    • .\gradlew.bat assembleRelease (or gradlew assembleRelease on Linux/macOS) is a command that builds the Android application from the existing android directory. It does not re-generate or modify the native project files based on your app.json or installed Expo packages. It simply compiles whatever native code is currently present in that directory.

Conclusion:

Since expo-localization adds a plugin that modifies native configuration, your existing android directory likely doesn't have these necessary changes. Therefore, if you just run .\gradlew.bat assembleRelease, the build will likely fail or the expo-localization features won't work correctly in the resulting APK.

---
Gem also suggested the steps for the rebuild. I brought in some small variations and used them after confirming with Gem that they are OK. The steps I used:

npx expo prebuild --clean --platform android

npx expo-doctor
Its output:
npx expo-doctor
14/15 checks passed. 1 checks failed. Possible issues detected:
Use the --verbose flag to see more details about passed checks.

✖ Check that packages match versions required by installed Expo SDK
The following packages should be updated for best compatibility with the installed expo version:
  expo@53.0.10 - expected version: 53.0.11
Your project may not work correctly until you install the expected versions of the packages.
Found outdated dependencies
Advice:
Use 'npx expo install --check' to review and upgrade your dependencies.

1 check failed, indicating possible issues with the project.
----

npx expo install --check
Said y to prompt(s).
TimestampTrackerWeb> npx expo-doctor
15/15 checks passed. No issues detected!
TimestampTrackerWeb> 
---

Copied my-release-key.keystore file to android/app/my-release-key.keystore
Edited android/gradle.properties and added (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
----

Then edited android/app/build.gradle and near the bottom of android { ... } section, added:

signingConfigs {
    release {
        storeFile file(MYAPP_UPLOAD_STORE_FILE)
        storePassword MYAPP_UPLOAD_STORE_PASSWORD
        keyAlias MYAPP_UPLOAD_KEY_ALIAS
        keyPassword MYAPP_UPLOAD_KEY_PASSWORD
    }
}
buildTypes {
    release {
        signingConfig signingConfigs.release
        minifyEnabled false
        shrinkResources false
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}
---------

cd android
.\gradlew.bat clean
Key part of its output:
Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/8.13/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD SUCCESSFUL in 18s
38 actionable tasks: 24 executed, 14 up-to-date
...
Shared above with Gem. Gem said its OK.
----
.\gradlew.bat assembleRelease
Key part of its output:
Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/8.13/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD SUCCESSFUL in 5m 18s
307 actionable tasks: 283 executed, 24 up-to-date
...
Shared above with Gem. Gem said its OK.
I noted that the build did not involve any significant download as I did not see any downloading messages while the build was going on.
...
...TimestampTrackerWeb\android\app\build\outputs\apk\release has app-release.apk of size 57.2 MB.

Was able to install it on phone (copied APK using pairdrop.net). It updated older app and seems to be running OK. Updated Google Drive shared APK with this release APK file.
-----------

Next I am exploring publishing the app to a free store. Not Google Play as I don't have a developer a/c there and I don't want to pay it $25 or whatever to open an a/c.

I looked at F-Droid . That seems to have a complicated process to publish an app and its docs seem to provide Linux based instructions.

Amazon App Store is discontinuing apps meant only for android devices.

Uptodown - https://en.uptodown.com/developers-zone - seems interesting and has an anti-virus check of apps which makes it more safe for people to download my app.

Checked with Gem about the above.  It said: Uptodown is indeed a popular and developer-friendly platform.
  • Free Publishing: As you noted, it's completely free to publish apps there, which is a major advantage over Google Play.
  • Web-Centric: A significant portion of Uptodown's traffic comes from its website, making it highly accessible for users to download APKs directly without needing a dedicated app store app. This broadens your reach.
  • Security Checks: Their use of VirusTotal for scanning apps is a positive for user safety and trust.
  • Developer Console: They provide a developer console for submitting and managing your apps, and their help documentation indicates a relatively straightforward process for publishing an APK.
  • No Profit Sharing: For free apps or apps with your own monetization, they don't take a cut, which is ideal.
Gem mentioned some other options of which I am sharing two, below:
  1. APKMirror:
    1. Pros: Very popular and trusted for sideloading APKs. They manually vet submissions for authenticity. No developer account or fee.
    2. Cons: Primarily a repository for APKs rather than a full "app store" experience. Users often go there to find specific versions or region-locked apps, not necessarily to browse for new ones. Monetization might be harder if that's a long-term goal. You submit an APK, but it's more about hosting than discovery.
  2. Aptoide:
    1. Pros: Decentralized app store model, allows users to create their own "stores." Free to publish. No geographical restrictions.
    2. Cons: Because of its decentralized nature, there can be a higher perceived risk of unofficial or modified apps, though Aptoide does have its own security measures. You'll need to create a developer account.
It also mentioned Samsung Galaxy Store but when I had browsed on it earlier today, I read somewhere that apps published there need to have some Samsung differentiator or something, meaning the app needs to use some Samsung specific stuff.

Gem also mentioned Huawei AppGallery (popular in China) and GetJar (one of the oldest app stores) but I don't think they are suitable for me.

Next, I created an account on https://en.uptodown.com/developers-zone .

Chose publish app ... Uploading APK (showed size as 60 MB) ... Upload speed seems to be 2 Mbps ... seems to have got stuck at 85% upload though uploading still seems to be going on as per Net Speed Meter ... after perhaps a minute or two it moved further from 85% figure and completed upload quickly after that. Net Speed Meter shows very low upload (low Kbps).

In General Specs: Category -> General, Loaded icon file used by app, Kept official website unfilled,
Author -> Nationality - India, Developer Name: Ravi S. Iyer

Uploaded 2 (English language) screenshots - Light & Dark modes pics

In Descriptions, it says, "For each of the languages available on Uptodown, you can add both a short and a full body text description of your application. We strongly urge you to ensure the text is well-written. Specifically, the Full body text description should contain at least 100 words. Should the description fail to meet the required quality standards, our editorial team reserves the right to reject its publication, make modifications, or replace it."

Language English
Short description (max 70 ch): Simple launch and one-touch-add timestamp recorder without notes
Long description (min. 50 words) [H2, H3, B, I, U, Link]
This Timestamp Tracker app is a simple launch and one-touch-add timestamp recorder app with no text associated with the timestamp. It automatically creates a timestamp when the app is launched. As it has been developed using React Native and Expo, there is an Android app version and a web version.

The app will display a hardcoded (100) maximum number of timestamps.
It has buttons to:
* Add a timestamp
* Export timestamps data as .csv
* Clear the timestamps
* Show app info.
It also shows duration from previous timestamp. 

This app is useful to quickly and easily record timestamps and also to capture typically small break duration or even small task duration. The app supports light and dark modes and uses the device settings for it.

[url=https://ravisiyer.github.io/CGPTTimestamp/]Web app deployed on GitHub Pages[/url] 
Blog post: [url=https://raviswdev.blogspot.com/2025/06/using-chatgpt-to-write-react-native.html]Using ChatGPT and Gemini to write React Native and Expo Timestamp app (mobile and web)[/url] 
---------

I clicked 'Submit for Review'. It showed a confirm message:
Send to our editors
Are you sure you want to send your app for review? We'll be checking it as soon as possible. The review process can take a few days depending on the volume of submissions received. You will not be able to modify any app details after clicking on 'Submit for review'.
----

I chose Confirm.

Then it gave a message about at least 3 screenshots needed.

I added an Info screenshot and then clicked 'Submit for Review' again and chose Confirm in the dialog. Then I got the message: App submitted successfully. Now 'Submit for Review' button is disabled. 
Just under the app title - Timestamp Tracker - in a yellow box with hard to read white text, it says: PENDING REVISION. [Strange! Should have 'PENDING REVIEW']
----------

https://en.uptodown.com/developers-zone/services  indicates it is a Spanish company.
----

Is Uptodown App Store Safe?, https://www.youtube.com/watch?v=lVr1KRX8mkw , 2 min. 8 secs.

Uptodown uses VirusTotal to check APK for virus. Decided to try it out myself as above video showed that one could upload APK files to the site to check for virus.

https://www.virustotal.com/gui/home/upload -> for my release APK file, gave some error message about zip file containing more than one file and exceeding 3 MB in size but still gave option to confirm upload. Going through it now ... The upload speed is much faster (2 to 5 Mbps) ... I think I could have also given the Google Drive share link ... Upload got over perhaps within a minute (57.27 MB). It started and finished its "Security vendors' analysis" quite quickly - perhaps less than 30 seconds. Then it gave the message: "No security vendors flagged this file as malicious".
Few of the vendors listed:
Acronis (Static ML)
Avast
Avast-Mobile
AVG
Avira (no cloud)
Baidu
BitDefender
----

Some "BEHAVIOR" check shows a spinning icon ... "There are some sandboxes still analysing the file." ... this is taking minutes ...

When I visited above link after some time, the spinning icon was not shown. 
I think now I understand this screen better. It shows two 'sandbox' reports summary. One is VirusTotal R2DBox which had a checked box against it the last time I saw it. So perhaps that had checked 'behavior' and it said it was OK. The second one (to the right): Zenbox android, IFIRC, had the spinning icon against it the last time. This time around, it too shows a checked box.
Choosing full report for VirusTotal R2DBox showed a page which expired in a short time. That page was long but did not have an overview section and was hard to understand.

Choosing full report for Zenbox android showed another page which had an overview section:
Overview
Zenbox Android Verdict
52/100
Non Malicious
Report generated:
12/06/2025 15:12:18
Guest System:
Android Ultimate
-------------

I created an account with https://www.virustotal.com/ .

Trying to upload APK release file again resulted in it detecting that it was the earlier upload (done with anonymous) and I was shown the analysis screen right away.

On checking with Gem about longevity of the virustotal.com scan report for my APK and whether I should share that report link in this post, Gem said:

Yes, you absolutely should mention it in your blog post and share the report link!

Here's why and what to consider:

  1. Persistence of VirusTotal Links: VirusTotal reports are generally highly persistent. Once a file is uploaded and scanned, its hash is recorded, and the report is linked to that hash. As long as the file (and therefore its hash) remains exactly the same, the report URL will continue to point to that specific scan result. They are designed to be a permanent record of a file's analysis.
  2. Building Trust and Transparency: Sharing a link to an independent, third-party security scan like VirusTotal is a fantastic way to build trust with your potential users. In an era where users are increasingly concerned about app security and privacy, this level of transparency is a significant positive. It shows you're confident in your app's integrity and willing to provide external verification.
  3. Positive User Experience: Users who might be hesitant to download an APK directly (especially from a lesser-known developer or platform) will appreciate having this reassurance.
-------------
As this post has become very long, I am continuing this post as another post: Continued ... React Native and Expo Timestamp app (mobile and web) - part 2.


Comments