diff --git a/.claude/skills/test-subapp-locally/SKILL.md b/.claude/skills/test-subapp-locally/SKILL.md new file mode 100644 index 00000000..1ad7796d --- /dev/null +++ b/.claude/skills/test-subapp-locally/SKILL.md @@ -0,0 +1,186 @@ +--- +name: test-subapp-locally +description: Run a local FeedTheMonster (FTM) or assessment-survey-js build inside the CRContainer Android app for end-to-end testing — redirect the deployed sub-app URL to a local dev server, set up port forwarding, link a local @curiouslearning/core, and verify the JS→native payload bridge and Firestore writes. Use whenever testing sub-app changes, the AppEventPayload bridge, analytics/metadata payloads, or any FTM/Assessment behavior against a real container build. +argument-hint: Which sub-app (FTM or Assessment), and whether you're also testing local @curiouslearning/core changes. +--- + +# Test a sub-app locally inside CRContainer + +CRContainer loads sub-apps (FTM, Assessment) from the web URLs in its manifest. To test a +**local** sub-app build instead, the debug build rewrites the sub-app's URL to a local dev +server. This skill covers the full rig: URL redirect, port forwarding, optional local `core` +link, feature flags, and how to verify the payload bridge → Firestore. + +## How the redirect works + +In **debug builds only**, `WebAppsAdapter.maybeOverrideAppUrlForLocalDev()` +([WebAppsAdapter.java](../../../app/src/main/java/org/curiouslearning/container/presentation/adapters/WebAppsAdapter.java)) rewrites a sub-app URL when its host matches a configured list: + +- Driven by two `BuildConfig` fields that [app/build.gradle](../../../app/build.gradle) reads from + your gitignored `local.properties`: + - `LOCAL_SUBAPP_MATCH_HOSTS` — comma-separated hosts to redirect (empty by default → no redirect) + - `LOCAL_SUBAPP_REPLACEMENT_ORIGIN` — the local origin to redirect to (e.g. `http://localhost:8080`) +- It keeps only `scheme://authority`, **drops the path** (the dev server serves the app at root), + and **preserves the query/fragment** (e.g. `?cr_lang=english`, `?data=hausa-sightwords`). +- Empty defaults live in `defaultConfig`, so release builds are never affected. Real values only + ever exist in `local.properties`, which CI does not have — so the redirect is inert in the debug + APK too. It is active **only** when you build from your own machine. + +After the redirect, `WebApp.addContainerVersionToUrl()` appends `container_app_version=`. + +## Step-by-step + +### 1. Find the sub-app's current host (it changes per environment) + +The debug build loads its manifest from the `testing_branch` API +(`BuildConfig.API_URL` → `.../container_app_manifest/testing_branch/web_app_manifest.json`). +Fetch it and read the real `appUrl` host for your sub-app — don't assume: + +```bash +curl -s "https://devcuriousreader.wpcomstaging.com/container_app_manifest/testing_branch/web_app_manifest.json" \ + | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const j=JSON.parse(s);(j.web_apps||j.apps||j).forEach?.(a=>{}); const arr=j.web_apps||j.apps||j; (Array.isArray(arr)?arr:[]).forEach(a=>{const u=a.appUrl||a.app_url||a.url; if(u){try{console.log(new URL(u).host,' <- ',u)}catch(e){}}})})" +``` + +Known hosts as of this writing (verify each time — they migrate, e.g. to S3): + +| Sub-app | testing_branch host | Dev port | +|---|---|---| +| FTM | `globallit-aws-s3-static-webapp-test-us-east-2.s3.us-west-2.amazonaws.com` | 8080 | +| Assessment | `assessmentdev.curiouscontent.org` | 8081 | + +(Prod/dev fallback hosts: `feedthemonster.curiouscontent.org`, `feedthemonsterdev.curiouscontent.org`, `assessment.curiouscontent.org`.) + +### 2. Point the debug build at your local dev server + +Set both keys in **`local.properties`** (repo root) — *not* in `build.gradle`: + +```properties +localSubappMatchHosts= +localSubappReplacementOrigin=http://localhost: +``` + +`local.properties` is gitignored, so these never get committed and are always absent on CI. That +matters: CI builds a debug APK and **commits it to the repo** for testers +([Fastfile](../../../app/fastlane/Fastfile)), so an active redirect in a tracked file would point +every tester at their own localhost. Config here is structurally incapable of that — nothing to +remember to revert. See [local.properties.example](../../../local.properties.example) for the +copy-paste block for each sub-app. + +Both keys must be set for the redirect to activate; blank or missing = no redirect. + +**Use `http://localhost`, not `http://10.0.2.2`.** Sub-apps register a **service worker**, which +only works in a secure context (HTTPS or `localhost`/`127.0.0.1`). `10.0.2.2` is treated as +insecure and the SW silently fails. `localhost` is reachable from the device via `adb reverse` +(step 4) and works for both emulator and physical device. + +`localhost` and `127.0.0.1` are cleartext-permitted for debug builds in +[app/src/debug/res/xml/network_security_config.xml](../../../app/src/debug/res/xml/network_security_config.xml) +(wired via the debug [AndroidManifest.xml](../../../app/src/debug/AndroidManifest.xml) +`networkSecurityConfig`). Without it `targetSdk 35` blocks the load with +`net::ERR_CLEARTEXT_NOT_PERMITTED`. Add new hosts there if you switch off loopback. + +Re-sync Gradle after editing `local.properties`. + +**Sub-app identity survives the redirect.** The rewritten URL no longer contains the real host or +the sub-app's name, so `WebApp` keeps a `localDevOriginalUrl` extra (set only when a redirect +happened) and resolves two things from it rather than the loaded URL: + +- `isFtmApp` — otherwise false under redirect, silently disabling the monster-evolution polling. +- `AppContextKey.HOSTNAME` → `attribution.hostname` in Firestore — otherwise recorded as + `localhost`, corrupting the field you're likely verifying. + +So expect the **real** sub-app host in `attribution.hostname` even while running locally. + +### 3. (Optional) Link a local `@curiouslearning/core` + +Only if you're also testing un-published `core` changes (the payload schema / `AndroidInterface` +live in `@curiouslearning/core`, a separate repo at `c:\CuriousLearning\core`): + +```bash +cd c:/CuriousLearning/core && npm run build && npm link +cd c:/CuriousLearning/FeedTheMonsterJS && npm link @curiouslearning/core # or assessment-survey-js +``` + +The dev-server build log should show `../core/dist/index.js [built]`, confirming the link is used. +Rebuild `core` (`npm run build`) after each core change. Unlink later with +`npm unlink @curiouslearning/core` then `npm install`. + +### 4. Start the dev server + forward the port + +```bash +# FTM +cd c:/CuriousLearning/FeedTheMonsterJS && npm run dev # serves :8080 +# Assessment +cd c:/CuriousLearning/assessment-survey-js && npm run dev # serves :8081 + +# Forward device localhost -> host dev server (re-run if the emulator/adb restarts) +adb reverse tcp:8080 tcp:8080 # match the dev port +adb reverse --list # verify +``` + +### 5. Enable the feature flags that gate the sub-app→native path + +- **FTM**: `mr-75` (`FEATURE_ANDROID_EVENT_BUBBLE`) must be enabled for your test user, or FTM + never registers the Android strategy and nothing bubbles. Loaded via `@curiouslearning/features` + (Statsig) — see [feedTheMonster.ts](../../../../FeedTheMonsterJS/src/feedTheMonster.ts) `initAndroidModule()`. +- **Assessment**: the summary path is gated by `enableAndroidSummary`; the user-sessions path fires + when `appType === Assessment.TYPE` — see assessment `App.ts` `notifySummaryData` / completion handler. + +### 6. Run the debug app and watch Logcat + +Run the **debug** variant on the connected device. Open the sub-app tile and confirm the redirect: + +``` +WebAppsAdapter: DEBUG sub-app URL override: https:///...? -> http://localhost:/? +``` + +Complete a level/puzzle (FTM) or finish an assessment, then watch the bridge + handler: + +``` +WebApp: BRIDGE_PARSED: collection=... app_id=... cr_user_id=... +WebApp: BRIDGE_VALIDATED -> handler.handle() +AppEventHandler: Handling summary_data / user_sessions_data payload +AppEventHandler: ... saved docId=... (or "Updated summary payload") +``` + +Entry point: `WebAppInterface.logMessage()` in +[WebApp.java](../../../app/src/main/java/org/curiouslearning/container/WebApp.java); +storage in [DefaultAppEventPayloadHandler.java](../../../app/src/main/java/org/curiouslearning/container/core/subapp/handler/DefaultAppEventPayloadHandler.java). + +### 7. Verify Firestore + +Check the `summary_data` and `user_sessions_data` collections for the new doc, with the expected +top-level fields (`cr_user_id`, `app_id`, `collection`, `metadata`, `data`, timestamps). + +## Gotchas + +- **Service worker caching**: the dev build writes `sw.js` and precaches the bundle. If the WebView + serves a stale sub-app, clear the container app's storage (or uninstall/reinstall) so it refetches. +- **`adb reverse` is not persistent**: it resets when the emulator or adb server restarts. Re-run it. +- **"Invalid Host header"** from webpack-dev-server: IP/localhost hosts pass by default; if it + appears, add `allowedHosts: 'all'` to the sub-app's `devServer` config. +- **Wrong host**: if you never see the override log, the manifest host changed — redo step 1. +- **Release safety**: never rely on path in the redirect; only `scheme://authority` + query survive. + +## Cleanup + +Nothing to revert in tracked files — the redirect config lives only in your gitignored +`local.properties`. Just tear down the forwarding when you're done: + +```bash +adb reverse --remove-all +``` + +Leave the `local.properties` keys in place (or comment them out) for next time. + +## Key files + +| File | Role | +|---|---| +| `local.properties` (gitignored) | `localSubappMatchHosts` / `localSubappReplacementOrigin` — your actual values | +| [local.properties.example](../../../local.properties.example) | tracked template documenting both keys | +| [app/build.gradle](../../../app/build.gradle) | reads those keys into `LOCAL_SUBAPP_*` `BuildConfig` fields (debug) + empty defaults | +| [WebAppsAdapter.java](../../../app/src/main/java/org/curiouslearning/container/presentation/adapters/WebAppsAdapter.java) | `maybeOverrideAppUrlForLocalDev()` redirect logic + `localDevOriginalUrl` extra | +| [app/src/debug/res/xml/network_security_config.xml](../../../app/src/debug/res/xml/network_security_config.xml) | cleartext allowlist (localhost, 127.0.0.1) | +| [WebApp.java](../../../app/src/main/java/org/curiouslearning/container/WebApp.java) | bridge entry `logMessage()`, `addContainerVersionToUrl()`, `BRIDGE_*` logs | +| [DefaultAppEventPayloadHandler.java](../../../app/src/main/java/org/curiouslearning/container/core/subapp/handler/DefaultAppEventPayloadHandler.java) | Firestore routing + `AppEventHandler` logs | diff --git a/app/build.gradle b/app/build.gradle index 0b057185..d8375384 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,6 +7,15 @@ plugins { id 'io.sentry.android.gradle' version '5.10.0' } def buildBranch = project.hasProperty("buildBranch") ? project.getProperty("buildBranch") : "main" + +// Developer-local settings (gitignored). Absent on CI, so anything read from here is always +// empty in a distributed APK. See local.properties.example. +def localProps = new Properties() +def localPropsFile = rootProject.file("local.properties") +if (localPropsFile.exists()) { + localPropsFile.withInputStream { localProps.load(it) } +} + def apiUrl = buildBranch == "main" ? "https://devcuriousreader.wpcomstaging.com/container_app_manifest/prod/" : "https://devcuriousreader.wpcomstaging.com/container_app_manifest/testing_branch/" @@ -45,6 +54,14 @@ android { // ipinfo.io Lite token — same public token shipped in the assessment-survey-js web bundle buildConfigField "String", "IPINFO_TOKEN", "\"b6268727178610\"" + + // Local sub-app dev redirect, consumed by WebAppsAdapter.maybeOverrideAppUrlForLocalDev(). + // Empty here so release builds compile and never redirect; the debug buildType below + // fills these from the developer's own (gitignored) local.properties, which means they + // are always empty on CI -> the redirect is inert in every distributed APK. + // See local.properties.example and the test-subapp-locally skill. + buildConfigField "String", "LOCAL_SUBAPP_MATCH_HOSTS", "\"\"" + buildConfigField "String", "LOCAL_SUBAPP_REPLACEMENT_ORIGIN", "\"\"" } buildFeatures { @@ -76,6 +93,12 @@ android { debug{ testCoverageEnabled true buildConfigField "String", "API_URL", "\"https://devcuriousreader.wpcomstaging.com/container_app_manifest/testing_branch/\"" + // Local sub-app dev redirect — values come from local.properties (gitignored, never + // committed, absent on CI). Copy local.properties.example to set them up. + buildConfigField "String", "LOCAL_SUBAPP_MATCH_HOSTS", + "\"${localProps.getProperty('localSubappMatchHosts', '')}\"" + buildConfigField "String", "LOCAL_SUBAPP_REPLACEMENT_ORIGIN", + "\"${localProps.getProperty('localSubappReplacementOrigin', '')}\"" } release { signingConfig signingConfigs.release diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..d4c89e31 --- /dev/null +++ b/app/src/debug/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/app/src/debug/res/xml/network_security_config.xml b/app/src/debug/res/xml/network_security_config.xml new file mode 100644 index 00000000..b01b7fa3 --- /dev/null +++ b/app/src/debug/res/xml/network_security_config.xml @@ -0,0 +1,21 @@ + + + + + localhost + 127.0.0.1 + + diff --git a/app/src/main/java/org/curiouslearning/container/MainActivity.java b/app/src/main/java/org/curiouslearning/container/MainActivity.java index 7240eaad..b7f70bf0 100644 --- a/app/src/main/java/org/curiouslearning/container/MainActivity.java +++ b/app/src/main/java/org/curiouslearning/container/MainActivity.java @@ -21,6 +21,7 @@ import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.Button; +import android.widget.EditText; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.ProgressBar; @@ -768,6 +769,13 @@ private void cachePseudoId() { private void warmFirestoreDataSync() { String pseudoId = prefs.getString("pseudoId", ""); + // Debug-only override mirrors WebApp.initViews so a tester's custom cr_user_id syncs on container open too. + if (BuildConfig.DEBUG) { + String customUserId = prefs.getString("custom_cr_user_id", ""); + if (customUserId != null && !customUserId.isEmpty()) { + pseudoId = customUserId; + } + } summaryHandler = DefaultAppEventPayloadHandler.getInstance(pseudoId); } public static String convertEpochToDate(long epochMillis) { @@ -916,6 +924,13 @@ private void showLanguagePopup() { TextInputLayout textBox = dialog.findViewById(R.id.dropdown_menu); AutoCompleteTextView autoCompleteTextView = dialog.findViewById(R.id.autoComplete); + // Debug-only: custom cr_user_id override field for testing. + final EditText customUserIdField = dialog.findViewById(R.id.custom_user_id); + if (BuildConfig.DEBUG && customUserIdField != null) { + customUserIdField.setVisibility(View.VISIBLE); + customUserIdField.setText(prefs.getString("custom_cr_user_id", "")); + } + // Ensure TextInputLayout has transparent background (Material Design can // override XML) textBox.setBackground(null); @@ -983,6 +998,7 @@ public void onChanged(List webApps) { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { audioPlayer.play(MainActivity.this, R.raw.sound_button_pressed); + persistCustomUserId(customUserIdField); String selectedDisplayName = (String) parent.getItemAtPosition(position); selectedLanguage = languagesEnglishNameMap.get(selectedDisplayName); @@ -1045,6 +1061,7 @@ public void run() { closeButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { audioPlayer.play(MainActivity.this, R.raw.sound_button_pressed); + persistCustomUserId(customUserIdField); textView.setVisibility(View.GONE); // Animate close button, then trigger dropdown exit animation @@ -1096,6 +1113,22 @@ public void run() { } } + // Debug-only: persist (or clear) the custom cr_user_id override entered in the + // language popup. No-op in release builds since the field is never shown there. + private void persistCustomUserId(EditText customUserIdField) { + if (!BuildConfig.DEBUG || customUserIdField == null) { + return; + } + String customId = customUserIdField.getText().toString().trim(); + SharedPreferences.Editor editor = prefs.edit(); + if (customId.isEmpty()) { + editor.remove("custom_cr_user_id"); + } else { + editor.putString("custom_cr_user_id", customId); + } + editor.apply(); + } + private Map MapLanguagesEnglishName(List webApps) { Map languagesEnglishNameMap = new TreeMap<>(); for (WebApp webApp : webApps) { diff --git a/app/src/main/java/org/curiouslearning/container/WebApp.java b/app/src/main/java/org/curiouslearning/container/WebApp.java index 24755f75..6d65cbfb 100644 --- a/app/src/main/java/org/curiouslearning/container/WebApp.java +++ b/app/src/main/java/org/curiouslearning/container/WebApp.java @@ -39,6 +39,10 @@ public class WebApp extends BaseActivity { private String title; private String appUrl; + // Debug-only: the deployed URL a local-dev redirect replaced, or null when not redirected. + // Identity checks (which sub-app this is, which host to attribute) resolve off this instead of + // appUrl so they keep matching the real sub-app while pointed at localhost. + private String localDevOriginalUrl; private WebView webView; private SharedPreferences sharedPref; @@ -79,18 +83,41 @@ private void getIntentData() { appUrl = intent.getStringExtra("appUrl"); language = intent.getStringExtra("language"); languageInEnglishName = intent.getStringExtra("languageInEnglishName"); + if (BuildConfig.DEBUG) { + localDevOriginalUrl = intent.getStringExtra("localDevOriginalUrl"); + } - String host = (appUrl != null) ? Uri.parse(appUrl).getHost() : null; + // Attribute to the deployed host even when a debug build is redirected to localhost, + // so test writes don't record "localhost" in the hostname field. + String identityUrl = identityUrl(); + String host = (identityUrl != null) ? Uri.parse(identityUrl).getHost() : null; AppContext.getInstance().set(AppContextKey.HOSTNAME, (host != null && !host.isEmpty()) ? host : "unknown"); } } + /** + * The URL to use when identifying the sub-app (which app it is, which host to attribute) rather + * than when loading it. Normally just {@link #appUrl}; in a debug build redirected to a local + * dev server it is the deployed URL the redirect replaced, since the local one carries neither + * the sub-app's name nor its host. + */ + private String identityUrl() { + return (localDevOriginalUrl != null && !localDevOriginalUrl.isEmpty()) ? localDevOriginalUrl : appUrl; + } + private void initViews() { sharedPref = getApplicationContext().getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); utmPrefs = getApplicationContext().getSharedPreferences(UTM_PREFS_NAME, Context.MODE_PRIVATE); isDataCached = sharedPref.getBoolean(String.valueOf(urlIndex), false); pseudoId = sharedPref.getString("pseudoId", ""); + // Debug-only override: a custom cr_user_id set via the language popup (testing). + if (BuildConfig.DEBUG) { + String customUserId = sharedPref.getString("custom_cr_user_id", ""); + if (customUserId != null && !customUserId.isEmpty()) { + pseudoId = customUserId; + } + } source = utmPrefs.getString("source", ""); campaignId = utmPrefs.getString("campaign_id", ""); goBack = findViewById(R.id.button2); @@ -114,8 +141,8 @@ private void loadWebView() { webView.setOverScrollMode(View.OVER_SCROLL_NEVER); webView.setHorizontalScrollBarEnabled(false); - // Check if this is FTM app - isFtmApp = appUrl.contains("feedthemonster"); + // Check if this is FTM app (off identityUrl so a local-dev redirect doesn't hide it) + isFtmApp = identityUrl().contains("feedthemonster"); // Create custom WebViewClient for FTM to handle monster state API webView.setWebViewClient(new WebViewClient() { diff --git a/app/src/main/java/org/curiouslearning/container/presentation/adapters/WebAppsAdapter.java b/app/src/main/java/org/curiouslearning/container/presentation/adapters/WebAppsAdapter.java index 5d084a31..11abb299 100644 --- a/app/src/main/java/org/curiouslearning/container/presentation/adapters/WebAppsAdapter.java +++ b/app/src/main/java/org/curiouslearning/container/presentation/adapters/WebAppsAdapter.java @@ -7,7 +7,9 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.net.Uri; import android.os.Handler; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -15,6 +17,7 @@ import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; +import org.curiouslearning.container.BuildConfig; import org.curiouslearning.container.R; import org.curiouslearning.container.data.model.WebApp; import org.curiouslearning.container.utilities.AnimationUtil; @@ -117,7 +120,15 @@ public void run() { Intent intent = new Intent(ctx, org.curiouslearning.container.WebApp.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra("appId", String.valueOf(webApps.get(position).getAppId())); - intent.putExtra("appUrl", webApps.get(position).getAppUrl()); + String appUrl = webApps.get(position).getAppUrl(); + String launchUrl = maybeOverrideAppUrlForLocalDev(appUrl); + intent.putExtra("appUrl", launchUrl); + if (BuildConfig.DEBUG && appUrl != null && !appUrl.equals(launchUrl)) { + // Debug-only: the redirect strips the real host from the URL, which would + // otherwise break FTM detection and the hostname attribution field. WebApp + // reads this to keep both resolving off the deployed URL. + intent.putExtra("localDevOriginalUrl", appUrl); + } intent.putExtra("title", webApps.get(position).getTitle()); intent.putExtra("language", webApps.get(position).getLanguage()); intent.putExtra("languageInEnglishName", webApps.get(position).getLanguageInEnglishName()); @@ -137,6 +148,54 @@ public void run() { public int getItemCount() { return webApps.size(); } + + /** + * Debug-only: redirects a deployed sub-app URL to a local dev server so a local FTM / + * Assessment build can be tested inside the real container. + * + *

Driven by {@code LOCAL_SUBAPP_MATCH_HOSTS} and {@code LOCAL_SUBAPP_REPLACEMENT_ORIGIN}, + * which the debug buildType reads from the developer's gitignored {@code local.properties}. + * Both are empty in release builds and on CI, so this is a no-op everywhere except a + * developer's own machine. See {@code local.properties.example}. + * + *

Only the origin is replaced: the original path is dropped (dev servers serve the app at + * root) while the query and fragment are preserved (e.g. {@code ?cr_lang=english}). + * + * @return the local URL when {@code appUrl}'s host is configured for redirect, otherwise + * {@code appUrl} unchanged. + */ + private String maybeOverrideAppUrlForLocalDev(String appUrl) { + if (!BuildConfig.DEBUG + || appUrl == null + || BuildConfig.LOCAL_SUBAPP_MATCH_HOSTS.isEmpty() + || BuildConfig.LOCAL_SUBAPP_REPLACEMENT_ORIGIN.isEmpty()) { + return appUrl; + } + Uri original = Uri.parse(appUrl); + String host = original.getHost(); + if (host == null) { + return appUrl; + } + for (String matchHost : BuildConfig.LOCAL_SUBAPP_MATCH_HOSTS.split(",")) { + if (!matchHost.trim().equalsIgnoreCase(host)) { + continue; + } + Uri replacement = Uri.parse(BuildConfig.LOCAL_SUBAPP_REPLACEMENT_ORIGIN); + StringBuilder rebuilt = new StringBuilder() + .append(replacement.getScheme()).append("://").append(replacement.getAuthority()).append('/'); + if (original.getEncodedQuery() != null) { + rebuilt.append('?').append(original.getEncodedQuery()); + } + if (original.getEncodedFragment() != null) { + rebuilt.append('#').append(original.getEncodedFragment()); + } + String overridden = rebuilt.toString(); + Log.d("WebAppsAdapter", "DEBUG sub-app URL override: " + appUrl + " -> " + overridden); + return overridden; + } + return appUrl; + } + @Override public void onViewRecycled(@NonNull ViewHolder holder) { super.onViewRecycled(holder); diff --git a/app/src/main/res/layout/language_popup.xml b/app/src/main/res/layout/language_popup.xml index c373a167..3a7a6878 100644 --- a/app/src/main/res/layout/language_popup.xml +++ b/app/src/main/res/layout/language_popup.xml @@ -90,6 +90,30 @@ android:textSize="18sp" /> + + + setting_box setting_box_close ... + Custom cr_user_id (testing) Offline mode indicator You\'re currently in offline mode Monster animation diff --git a/local.properties.example b/local.properties.example new file mode 100644 index 00000000..991a6d21 --- /dev/null +++ b/local.properties.example @@ -0,0 +1,48 @@ +## Developer-local settings — copy the keys you need into `local.properties`. +## +## `local.properties` is gitignored, so anything set here stays on your machine. CI has no +## `local.properties`, which is exactly why the local sub-app redirect below can never leak into +## the debug APK that CI builds and commits for testers. +## +## Android Studio manages the `sdk.dir` line in `local.properties` itself — don't copy one here. + + +## --------------------------------------------------------------------------------------------- +## Local sub-app redirect (debug builds only) +## --------------------------------------------------------------------------------------------- +## Makes the container load a sub-app (FTM / Assessment) from your local dev server instead of +## its deployed URL. Consumed by WebAppsAdapter.maybeOverrideAppUrlForLocalDev(). +## Full walkthrough: .claude/skills/test-subapp-locally/SKILL.md +## +## Both keys must be set for the redirect to activate; leave them out (or blank) to disable it. +## +## 1. localSubappMatchHosts — comma-separated hosts to redirect. Target ONE sub-app at a time: +## every matched host is rewritten to the same replacement origin below. Look the current host +## up in the testing_branch manifest rather than assuming — sub-apps migrate between hosts: +## curl -s "https://devcuriousreader.wpcomstaging.com/container_app_manifest/testing_branch/web_app_manifest.json" +## +## 2. localSubappReplacementOrigin — the local origin to load instead. Use `http://localhost`, +## NOT `http://10.0.2.2`: sub-apps register a service worker, which requires a secure context +## (HTTPS, localhost, or 127.0.0.1). 10.0.2.2 is treated as insecure and the worker silently +## fails to register. Cleartext to localhost/127.0.0.1 is already permitted for debug builds +## via app/src/debug/res/xml/network_security_config.xml. +## +## 3. Forward the device's localhost to your dev server before launching (match the port). +## Works for both an emulator and a physical device, and resets whenever the emulator or the +## adb server restarts, so re-run it if the redirect stops resolving: +## adb reverse tcp:8080 tcp:8080 +## adb reverse --list # verify +## +## Then re-sync Gradle and run from Android Studio. Confirm via logcat: +## WebAppsAdapter: DEBUG sub-app URL override: https:///... -> http://localhost:/?... +## +## Note the redirect only rewrites the origin: the original path is dropped (dev servers serve the +## app at root) while the query and fragment are preserved (e.g. ?cr_lang=english). + +## FTM — dev server on :8080 (cd c:/CuriousLearning/FeedTheMonsterJS && npm run dev) +#localSubappMatchHosts=globallit-aws-s3-static-webapp-test-us-east-2.s3.us-west-2.amazonaws.com +#localSubappReplacementOrigin=http://localhost:8080 + +## Assessment — dev server on :8081 (cd c:/CuriousLearning/assessment-survey-js && npm run dev) +#localSubappMatchHosts=assessmentdev.curiouscontent.org,assessment.curiouscontent.org +#localSubappReplacementOrigin=http://localhost:8081