ruby_everywhere 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +255 -0
  3. data/bridge/README.md +48 -0
  4. data/bridge/everywhere/bridge.js +388 -3
  5. data/bridge/everywhere/native.css +203 -0
  6. data/bridge/package.json +6 -3
  7. data/lib/everywhere/android_resources.rb +217 -0
  8. data/lib/everywhere/android_sdk.rb +330 -0
  9. data/lib/everywhere/asset_catalog.rb +185 -0
  10. data/lib/everywhere/auth_handoff.rb +398 -0
  11. data/lib/everywhere/auth_token.rb +118 -0
  12. data/lib/everywhere/builders/android.rb +844 -0
  13. data/lib/everywhere/builders/ios.rb +72 -3
  14. data/lib/everywhere/commands/build.rb +51 -9
  15. data/lib/everywhere/commands/clean.rb +10 -5
  16. data/lib/everywhere/commands/dev.rb +46 -8
  17. data/lib/everywhere/commands/doctor.rb +89 -3
  18. data/lib/everywhere/commands/install.rb +1 -1
  19. data/lib/everywhere/commands/logs.rb +117 -8
  20. data/lib/everywhere/config.rb +513 -14
  21. data/lib/everywhere/emulator.rb +405 -0
  22. data/lib/everywhere/engine.rb +21 -0
  23. data/lib/everywhere/icon.rb +196 -0
  24. data/lib/everywhere/log_filter.rb +74 -0
  25. data/lib/everywhere/mobile_config_endpoint.rb +13 -0
  26. data/lib/everywhere/mobile_configs_controller.rb +18 -0
  27. data/lib/everywhere/native_helper.rb +198 -5
  28. data/lib/everywhere/omniauth.rb +88 -0
  29. data/lib/everywhere/paths.rb +47 -0
  30. data/lib/everywhere/version.rb +1 -1
  31. data/support/mobile/android/README.md +217 -0
  32. data/support/mobile/android/app/build.gradle.kts +156 -0
  33. data/support/mobile/android/app/everywhere.properties +8 -0
  34. data/support/mobile/android/app/native-packages.gradle.kts +16 -0
  35. data/support/mobile/android/app/proguard-rules.pro +21 -0
  36. data/support/mobile/android/app/src/debug/res/xml/network_security_config.xml +30 -0
  37. data/support/mobile/android/app/src/main/AndroidManifest.xml +42 -0
  38. data/support/mobile/android/app/src/main/assets/everywhere.json +9 -0
  39. data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.codepoints +2235 -0
  40. data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.ttf +0 -0
  41. data/support/mobile/android/app/src/main/assets/json/path-configuration.json +19 -0
  42. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/AuthFlow.kt +268 -0
  43. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereApplication.kt +178 -0
  44. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereConfig.kt +409 -0
  45. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereEvents.kt +68 -0
  46. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereHost.kt +101 -0
  47. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/IconFont.kt +211 -0
  48. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainActivity.kt +975 -0
  49. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainTabs.kt +201 -0
  50. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebBottomSheetFragment.kt +13 -0
  51. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebFragment.kt +36 -0
  52. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/BiometricsComponent.kt +535 -0
  53. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/HapticsComponent.kt +141 -0
  54. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/MenuComponent.kt +361 -0
  55. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/NotificationComponent.kt +151 -0
  56. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/PermissionsComponent.kt +271 -0
  57. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/StorageComponent.kt +101 -0
  58. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/WebControlChannel.kt +204 -0
  59. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/extensions/EverywhereExtensions.kt +51 -0
  60. data/support/mobile/android/app/src/main/res/color/bottom_navigation_active_indicator.xml +4 -0
  61. data/support/mobile/android/app/src/main/res/color/bottom_navigation_item.xml +11 -0
  62. data/support/mobile/android/app/src/main/res/drawable/ic_launcher_foreground.xml +26 -0
  63. data/support/mobile/android/app/src/main/res/drawable/ic_tab_placeholder.xml +28 -0
  64. data/support/mobile/android/app/src/main/res/layout/activity_main.xml +116 -0
  65. data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet.xml +83 -0
  66. data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet_item.xml +35 -0
  67. data/support/mobile/android/app/src/main/res/layout/tab_more_sheet.xml +48 -0
  68. data/support/mobile/android/app/src/main/res/layout/tab_more_sheet_item.xml +35 -0
  69. data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +12 -0
  70. data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
  71. data/support/mobile/android/app/src/main/res/values/colors.xml +21 -0
  72. data/support/mobile/android/app/src/main/res/values/ic_launcher_background.xml +5 -0
  73. data/support/mobile/android/app/src/main/res/values/strings.xml +18 -0
  74. data/support/mobile/android/app/src/main/res/values/styles.xml +32 -0
  75. data/support/mobile/android/app/src/main/res/values/themes.xml +53 -0
  76. data/support/mobile/android/app/src/main/res/values-night/colors.xml +11 -0
  77. data/support/mobile/android/app/src/main/res/values-night/themes.xml +33 -0
  78. data/support/mobile/android/app/src/main/res/xml/network_security_config.xml +12 -0
  79. data/support/mobile/android/app/src/stamped/AndroidManifest.xml +34 -0
  80. data/support/mobile/android/build.gradle.kts +8 -0
  81. data/support/mobile/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  82. data/support/mobile/android/gradle/wrapper/gradle-wrapper.properties +10 -0
  83. data/support/mobile/android/gradle.properties +14 -0
  84. data/support/mobile/android/gradlew +251 -0
  85. data/support/mobile/android/gradlew.bat +94 -0
  86. data/support/mobile/android/settings.gradle.kts +28 -0
  87. data/support/mobile/ios/App/AppDelegate.swift +7 -1
  88. data/support/mobile/ios/App/AuthFlow.swift +101 -0
  89. data/support/mobile/ios/App/Bridge/MenuComponent.swift +192 -0
  90. data/support/mobile/ios/App/EverywhereConfig.swift +105 -0
  91. data/support/mobile/ios/App/SceneDelegate.swift +144 -0
  92. data/support/mobile/ios/App.xcodeproj/project.pbxproj +8 -0
  93. data/support/mobile/ios/README.md +10 -0
  94. metadata +69 -1
@@ -0,0 +1,217 @@
1
+ # RubyEverywhere Android Shell Template
2
+
3
+ A canonical [Hotwire Native Android][1] shell that wraps a deployed Ruby web app
4
+ (remote URL). The RubyEverywhere CLI stamps a copy of this directory per app —
5
+ the template itself is **never edited per-app**.
6
+
7
+ The iOS template is the reference implementation; this one is a feature-for-feature
8
+ port of `support/mobile/ios/`, and where the two differ the difference is forced
9
+ by the platform and is explained in a comment at the site.
10
+
11
+ ## Freeze rules
12
+
13
+ Every build script, layout, theme and Kotlin source here is written once and
14
+ frozen. When `every build --target android-*` stamps a copy, it overwrites
15
+ **exactly** these files and nothing else:
16
+
17
+ 1. `app/everywhere.properties` — applicationId, versionName, versionCode, app
18
+ label. The direct analogue of iOS's `App.xcconfig`: identity is stamped
19
+ without ever touching a build script. `app/build.gradle.kts` reads it at
20
+ configuration time.
21
+ 2. `app/src/main/assets/everywhere.json` — runtime config (remote URL, entry
22
+ path, colours, permissions, auth, universal-link hosts);
23
+ `Config#to_shell_json`.
24
+ 3. `app/src/main/assets/json/path-configuration.json` — baked rules +
25
+ `settings.tabs` (`Config#path_configuration_json("android")`). The same
26
+ document is served live at `/everywhere/android_v1.json` by the gem's
27
+ `MobileConfigEndpoint`, which the shell loads as its second, auth-aware
28
+ source.
29
+ 4. `app/src/main/assets/fonts/` — the selected Material icon font and its
30
+ `.codepoints` map. The bundled classic set
31
+ (`MaterialIcons-Regular.ttf`, 357 KB, 2,235 icons) is the offline fallback;
32
+ `icon_font: symbols` fetches the 10.6 MB variable font into
33
+ `~/.rubyeverywhere/android-fonts/` on first use. The two files must always
34
+ travel together — 282 names exist in both fonts with *different* codepoints.
35
+ 5. `app/src/main/res/values/colors.xml` and `values-night/colors.xml` — the
36
+ light and dark halves of `appearance.tint_color` / `background_color`.
37
+ Everything in the theme derives from those two colours.
38
+ 6. `app/src/main/res/values/ic_launcher_background.xml` and
39
+ `res/drawable-<dpi>/ic_launcher_foreground.png` — the adaptive launcher
40
+ icon's two layers. `minSdk 28` means there are no legacy
41
+ `mipmap-<dpi>/ic_launcher.png` fallbacks to generate.
42
+ 7. *(optionally)* `app/src/stamped/AndroidManifest.xml` — deep-link intent
43
+ filters, the auth callback scheme, and runtime `<uses-permission>` entries.
44
+ Written **only** when `everywhere.yml` declares `deep_linking:`, `auth:` or
45
+ `permissions:`. It is a separate manifest, folded in by the merger, so the
46
+ main manifest stays frozen — the analogue of iOS's optional
47
+ `App.entitlements`. See the note below on how it is wired.
48
+ 8. *(optionally)* `app/native-packages.gradle.kts` — Maven coordinates from
49
+ `native.android.packages`, and
50
+ `app/src/main/java/com/rubyeverywhere/shell/extensions/` — the app repo's
51
+ `native/android/*.kt` plus the generated `EverywhereExtensions.kt` registry.
52
+ Both ship as empty defaults so the raw template builds unchanged.
53
+ 9. `app/src/stamped/res/drawable*/` — the app's own images, generated from
54
+ `native/android/assets/**` by `Everywhere::AndroidResources`, reachable from
55
+ extension code as `R.drawable.<name>`. Registered as a second `res` srcDir on
56
+ `main` (see `sourceSets` in `app/build.gradle.kts`) and **cleared on every
57
+ build**, which is why it is its own directory rather than more files in
58
+ `main/res`: stamping merges over the work dir instead of wiping it, so an
59
+ asset the app deleted has to be removed by the generator that wrote it.
60
+ Filenames are the whole API and match iOS's exactly — `logo@2x~dark.png`
61
+ becomes `drawable-night-xhdpi/logo.png`.
62
+ 10. *(dev only)* `app/src/debug/assets/dev-url.txt` — the dev-server URL.
63
+ Android has no `SIMCTL_CHILD_*`, so `every dev --android` stamps this and
64
+ rebuilds, which it has to do anyway. Read only in debug builds; the debug
65
+ source set never ships in a release.
66
+
67
+ **Structural changes** — new source files, new dependencies, a plugin bump, a
68
+ new layout — mean deliberately editing this template and recommitting, never
69
+ editing a stamped copy.
70
+
71
+ ### Why `src/stamped/` is wired the way it is
72
+
73
+ AGP merges one manifest per *source set*, and without product flavours a variant
74
+ has exactly two: `main` and its build type. The generated manifest is therefore
75
+ registered as **both** build types' manifest in `app/build.gradle.kts`:
76
+
77
+ ```kotlin
78
+ sourceSets {
79
+ getByName("debug") { manifest.srcFile("src/stamped/AndroidManifest.xml") }
80
+ getByName("release") { manifest.srcFile("src/stamped/AndroidManifest.xml") }
81
+ }
82
+ ```
83
+
84
+ A product flavour would have been the textbook answer, but it renames every task
85
+ (`assembleStampedDebug`), and the builder, the dev loop and this README all
86
+ depend on plain `:app:assembleDebug`.
87
+
88
+ That leaves the build-type manifest slot occupied, so the debug-only cleartext
89
+ policy is expressed as a **resource overlay** instead:
90
+ `src/debug/res/xml/network_security_config.xml` overrides the release-safe
91
+ `src/main/res/xml/network_security_config.xml` of the same name. The main
92
+ manifest's `android:networkSecurityConfig` attribute never changes.
93
+
94
+ ## Dependency pin policy
95
+
96
+ The SPM equivalent. Plugin versions are pinned in the root `build.gradle.kts`,
97
+ library versions in `app/build.gradle.kts`, and both are **exact** — no ranges,
98
+ no `+`, no version catalog. Moving a pin is a deliberate edit here.
99
+
100
+ | | pinned at |
101
+ | --- | --- |
102
+ | Gradle | 9.2.0 (`-bin`, see below) |
103
+ | Android Gradle Plugin | 8.13.2 |
104
+ | Kotlin / kotlinx.serialization plugin | 2.3.0 |
105
+ | `dev.hotwire:core`, `dev.hotwire:navigation-fragments` | 1.3.0 |
106
+ | `compileSdk` / `targetSdk` | 35 |
107
+ | `minSdk` | 28 — Hotwire Native Android's floor, not negotiable |
108
+ | Java toolchain / JVM target | 17 |
109
+
110
+ The wrapper deliberately uses `gradle-9.2.0-**bin**.zip`, not `-all`: the `-all`
111
+ distribution unpacks to 522 MB in `GRADLE_USER_HOME` and the difference is
112
+ Gradle's own sources and javadoc, which only an IDE indexing the Gradle API ever
113
+ reads.
114
+
115
+ `settings.gradle.kts` keeps the `foojay-resolver-convention` plugin so the Java
116
+ toolchain can be provisioned independently of the launcher JVM — without it the
117
+ build fails on a machine whose only JDK is 21.
118
+
119
+ ## Building standalone
120
+
121
+ The raw template builds green with placeholder values, unsigned:
122
+
123
+ ```
124
+ export JAVA_HOME=$(/usr/libexec/java_home -v 17)
125
+ export ANDROID_HOME=$HOME/Library/Android/sdk
126
+ ./gradlew :app:assembleDebug
127
+ ```
128
+
129
+ Output: `app/build/outputs/apk/debug/app-debug.apk`.
130
+
131
+ The first build downloads roughly 1.4 GB (Gradle, the Android Gradle Plugin, the
132
+ Hotwire artifacts) and takes a couple of minutes, almost all of it network. Warm
133
+ no-op rebuilds are around 7 seconds. The CLI redirects `GRADLE_USER_HOME` to
134
+ `~/.rubyeverywhere/android-gradle` so that cache is shared across apps and never
135
+ lands inside the installed gem.
136
+
137
+ ## Dev loop
138
+
139
+ The shell is remote-mode only (`remote_url` in `everywhere.json`). For local
140
+ development, `every dev --android` stamps `src/debug/assets/dev-url.txt`,
141
+ rebuilds, reinstalls and relaunches — the Android equivalent of pressing `i` in
142
+ the iOS loop. The URL resolves down a three-step ladder:
143
+
144
+ 1. `adb reverse tcp:<port> tcp:<port>`, then `http://127.0.0.1:<port>` — correct
145
+ on emulators *and* physical devices. Preferred.
146
+ 2. `http://10.0.2.2:<port>` — the emulator's alias for the host loopback, used
147
+ automatically when `adb reverse` fails.
148
+ 3. An explicit `--dev-url` LAN address, for physical devices.
149
+
150
+ Cleartext to those hosts is allowed only in debug builds, and only for the hosts
151
+ themselves — never `cleartextTrafficPermitted` globally. See
152
+ `app/src/debug/res/xml/network_security_config.xml`.
153
+
154
+ ## Shape of the shell
155
+
156
+ | file | role |
157
+ | --- | --- |
158
+ | `EverywhereApplication.kt` | Hotwire config, bridge + destination + route-handler registration, first path-configuration load |
159
+ | `EverywhereConfig.kt` | `everywhere.json` decode; roots, dev override, instance override, auth, deep links |
160
+ | `EverywhereEvents.kt` | page → shell control messages, replacing iOS's NotificationCenter |
161
+ | `MainActivity.kt` | the SceneDelegate counterpart: tabs, splash, reset, badges, deep links, sign-in |
162
+ | `MainTabs.kt` | `settings.tabs` → the fixed pool of five NavigatorHosts |
163
+ | `IconFont.kt` | Material icons resolved by name from the bundled font |
164
+ | `WebFragment.kt`, `WebBottomSheetFragment.kt` | the web destinations |
165
+ | `bridge/` | the six `everywhere--*` bridge components and the control channel |
166
+ | `AuthFlow.kt` | third-party sign-in in a Custom Tab |
167
+ | `EverywhereHost.kt` | what app-authored `native/android/` code calls: `everywhereVisit`, `everywhereFetch` |
168
+ | `extensions/EverywhereExtensions.kt` | generated app-extension registry (empty by default) |
169
+
170
+ ### What app-authored screens get
171
+
172
+ `native/android/*.kt` compiles into `extensions/` and must declare
173
+ `package com.rubyeverywhere.shell.extensions` — the generated registry names
174
+ those types unqualified, and Kotlin ties nothing to the directory, so the package
175
+ line is what makes them referenceable. `every build` refuses a file without it.
176
+
177
+ The two helpers in `EverywhereHost.kt` are named to match iOS's
178
+ `EverywhereHost.swift` so extension code reads the same on both platforms:
179
+
180
+ * `everywhereVisit("/path")` — route through the active navigator, so a native
181
+ screen's links get the full path-configuration treatment (modals, other native
182
+ screens) like any web-driven visit.
183
+ * `everywhereFetch("/builds.json")` — GET from the app's own server carrying the
184
+ WebView's session cookie and the native User-Agent, so `native_app?` holds
185
+ server-side and an authenticated endpoint answers. Suspending; throws
186
+ `EverywhereFetchError` on a non-2xx.
187
+
188
+ Images live in `native/android/assets/**` and arrive as `R.drawable.<name>`
189
+ (freeze rule 9). There is no `everywhereHost` counterpart to iOS's — a Hotwire
190
+ Android destination is already a `Fragment`.
191
+
192
+ ### Three invariants that will break the app if they are violated
193
+
194
+ Measured on a device during the Phase 0 spike, not inferred:
195
+
196
+ 1. **`navigatorConfigurations()` must return a configuration for all five hosts,
197
+ on every launch.** `NavigatorHost.onCreate` throws
198
+ `IllegalStateException("No configuration found for NavigatorHost")` for any
199
+ host in `activity_main.xml` it can't find one for. "Not a tab right now" is
200
+ `isVisible = false`, **never** a shorter list — `switchTab` only hides hosts
201
+ present in the new list, so a dropped host stays on screen.
202
+ 2. **`lazyLoadTabs = true` is mandatory.** With eager loading all five pooled
203
+ hosts cold-boot a WebView and fetch their start location at launch,
204
+ including the hidden auth-gated ones.
205
+ 3. **A second `load()` is cheap but lossy.** Fragments and WebViews survive
206
+ (~96 ms), but `loadMenu()` opens with `menu.clear()`, which destroys badges
207
+ and icons, and resets Material's `selectedItemId` to 0 — which then takes the
208
+ *reselect* path and clears the selected tab's backstack whenever the selected
209
+ tab is the first one. `MainActivity.applyTabs` is the only caller and does
210
+ all three fix-ups; nothing else may call `load()`.
211
+
212
+ Tab **path** changes are not covered by any of that: `NavigatorHost` caches its
213
+ configuration in `onCreate`, so a changed `startLocation` needs
214
+ `Activity.recreate()`. `MainTabs` keeps two fingerprints so the cheap path is
215
+ taken whenever it can be.
216
+
217
+ [1]: https://github.com/hotwired/hotwire-native-android
@@ -0,0 +1,156 @@
1
+ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2
+ import java.util.Properties
3
+
4
+ plugins {
5
+ id("com.android.application")
6
+ id("org.jetbrains.kotlin.android")
7
+ id("org.jetbrains.kotlin.plugin.serialization")
8
+ }
9
+
10
+ // Per-app identity, read from a properties file rather than written into this
11
+ // script. `every build` overwrites everywhere.properties and nothing else here
12
+ // — the same contract as iOS's App.xcconfig, for the same reason: a build
13
+ // script the CLI edits is a build script that drifts.
14
+ val everywhere = Properties().apply {
15
+ file("everywhere.properties").inputStream().use { load(it) }
16
+ }
17
+
18
+ fun stamped(key: String, default: String): String =
19
+ everywhere.getProperty("everywhere.$key")?.takeIf { it.isNotBlank() } ?: default
20
+
21
+ android {
22
+ // The namespace is the R class / BuildConfig package and is frozen. It is
23
+ // deliberately NOT the applicationId: apps rename their bundle id freely,
24
+ // and a namespace change would move every generated class.
25
+ namespace = "com.rubyeverywhere.shell"
26
+ compileSdk = 35
27
+
28
+ defaultConfig {
29
+ applicationId = stamped("applicationId", "com.rubyeverywhere.app")
30
+ versionName = stamped("versionName", "0.1.0")
31
+ versionCode = stamped("versionCode", "1").toInt()
32
+
33
+ // minSdk 28 is Hotwire Native Android's floor and is not negotiable.
34
+ // It also clears API 26 for Typeface.Builder.setFontVariationSettings,
35
+ // which IconFont needs for the Material Symbols FILL axis.
36
+ minSdk = 28
37
+ targetSdk = 35
38
+
39
+ // Consumed by AndroidManifest.xml. The label is a placeholder so the
40
+ // manifest itself stays frozen when the app is renamed.
41
+ manifestPlaceholders["everywhereLabel"] = stamped("label", "RubyEverywhere")
42
+ }
43
+
44
+ sourceSets {
45
+ // Deep-link intent filters, the auth callback scheme and runtime
46
+ // <uses-permission> entries are generated into their own manifest so
47
+ // the main one never changes. AGP merges one manifest per build type,
48
+ // and the generated file is identical for both — hence the same path
49
+ // twice. See src/stamped/AndroidManifest.xml.
50
+ getByName("debug") { manifest.srcFile("src/stamped/AndroidManifest.xml") }
51
+ getByName("release") { manifest.srcFile("src/stamped/AndroidManifest.xml") }
52
+
53
+ // The app's own images (native/android/assets/**) are generated into
54
+ // src/stamped/res as drawables. A second res dir on `main` rather than
55
+ // more files in main/res, because the generator owns the directory
56
+ // outright and clears it every build — a deleted asset has to stop
57
+ // shipping, and stamping merges over the work dir instead of wiping it.
58
+ //
59
+ // On `main`, deliberately, not on a build type: same-variant duplicates
60
+ // are a merge ERROR, so an asset that collides with a template drawable
61
+ // fails the build instead of silently overriding it. AGP ignores the
62
+ // directory when it doesn't exist, which is how the raw template builds.
63
+ getByName("main") { res.srcDir("src/stamped/res") }
64
+ }
65
+
66
+ buildTypes {
67
+ getByName("debug") {
68
+ isDebuggable = true
69
+ }
70
+ release {
71
+ isMinifyEnabled = false
72
+ proguardFiles(
73
+ getDefaultProguardFile("proguard-android-optimize.txt"),
74
+ "proguard-rules.pro"
75
+ )
76
+ }
77
+ }
78
+
79
+ buildFeatures {
80
+ // BuildConfig.DEBUG gates web-view debugging, verbose logging and the
81
+ // dev-server asset. Nothing else in the shell branches on build type.
82
+ buildConfig = true
83
+ }
84
+
85
+ compileOptions {
86
+ sourceCompatibility = JavaVersion.VERSION_17
87
+ targetCompatibility = JavaVersion.VERSION_17
88
+ }
89
+
90
+ lint {
91
+ // `lintVitalRelease` crashes on this exact toolchain (AGP 8.13.2 +
92
+ // Kotlin 2.3.0). Its build-script visitor builds light classes for
93
+ // every `.gradle.kts` in the project and dies inside the Kotlin
94
+ // compiler with "findFirCompiledSymbol only works on compiled
95
+ // declarations" the moment one script `apply(from:)`s another — which
96
+ // native-packages.gradle.kts is, by design.
97
+ //
98
+ // Verified by bisection: commenting out the `apply(from:)` below makes
99
+ // lintVitalAnalyzeRelease pass, and nothing else does. So this disables
100
+ // a check that cannot run, not one that was failing us. Revisit when
101
+ // either version moves.
102
+ checkReleaseBuilds = false
103
+ }
104
+
105
+ packaging {
106
+ // The icon font is already a compressed container; deflating it again
107
+ // costs build time and saves nothing.
108
+ resources.excludes += "/META-INF/{AL2.0,LGPL2.1}"
109
+ }
110
+ }
111
+
112
+ kotlin {
113
+ compilerOptions {
114
+ jvmTarget.set(JvmTarget.JVM_17)
115
+ }
116
+ }
117
+
118
+ java {
119
+ toolchain {
120
+ languageVersion = JavaLanguageVersion.of(17)
121
+ }
122
+ }
123
+
124
+ dependencies {
125
+ // Hotwire Native. Pinned exactly, not to a range: this is the template's
126
+ // dependency pin policy, the analogue of iOS's Package.resolved.
127
+ implementation("dev.hotwire:core:1.3.0")
128
+ implementation("dev.hotwire:navigation-fragments:1.3.0")
129
+
130
+ // Material — BottomNavigationView, badges, bottom sheets, theming.
131
+ implementation("com.google.android.material:material:1.12.0")
132
+
133
+ // activity_main.xml's root.
134
+ implementation("androidx.constraintlayout:constraintlayout:2.2.1")
135
+
136
+ // WebViewCompat: addWebMessageListener (the control channel) and
137
+ // addDocumentStartJavaScript (window.__EVERYWHERE_CONFIG__). Android
138
+ // WebView has no user-script API of its own; these are the substitutes.
139
+ implementation("androidx.webkit:webkit:1.12.1")
140
+
141
+ // Custom Tabs — the ASWebAuthenticationSession stand-in for third-party
142
+ // sign-in, and what Hotwire's own BrowserTabRouteDecisionHandler uses.
143
+ implementation("androidx.browser:browser:1.8.0")
144
+
145
+ // Biometrics + the encrypted store they gate.
146
+ implementation("androidx.biometric:biometric:1.1.0")
147
+ implementation("androidx.security:security-crypto:1.1.0-alpha06")
148
+
149
+ // everywhere.json, the bridge components' message payloads, and the
150
+ // control channel's postMessage bodies.
151
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
152
+ }
153
+
154
+ // GENERATED: Maven coordinates from `native.android.packages`. Applied last so
155
+ // an app's own dependency can override a version above if it must.
156
+ apply(from = "native-packages.gradle.kts")
@@ -0,0 +1,8 @@
1
+ # STAMPED by `every build --android`. The direct analogue of iOS's
2
+ # App.xcconfig: identity lives here so no build script is ever edited per app.
3
+ # The values below are the template's placeholders — the raw template builds
4
+ # green with them.
5
+ everywhere.applicationId=com.rubyeverywhere.app
6
+ everywhere.versionName=0.1.0
7
+ everywhere.versionCode=1
8
+ everywhere.label=RubyEverywhere
@@ -0,0 +1,16 @@
1
+ // GENERATED by `every build --android` from `native.android.packages` in
2
+ // config/everywhere.yml. The checked-in copy is the empty default, so the raw
3
+ // template builds unchanged.
4
+ //
5
+ // native:
6
+ // android:
7
+ // packages:
8
+ // - "com.airbnb.android:lottie:6.4.0"
9
+ //
10
+ // This is a script plugin applied from app/build.gradle.kts, so it has no
11
+ // typed `implementation(...)` accessor — dependencies are added by
12
+ // configuration name. Keeping the generated surface to a single file means
13
+ // the frozen build script never changes shape when an app adds a package.
14
+ dependencies {
15
+ // add("implementation", "group:artifact:version")
16
+ }
@@ -0,0 +1,21 @@
1
+ # Bridge components are instantiated reflectively through
2
+ # BridgeComponentFactory and named by string from the page, so R8 has no call
3
+ # graph reaching them. Without this the release build strips every component
4
+ # and the page's `everywhere--*` messages silently go nowhere.
5
+ -keep class com.rubyeverywhere.shell.bridge.** { *; }
6
+
7
+ # App-provided native code (native/android/*.kt), same reasoning: it is
8
+ # referenced by identifier from the path configuration, not from Kotlin.
9
+ -keep class com.rubyeverywhere.shell.extensions.** { *; }
10
+
11
+ # Fragment destinations are resolved from the navigation graph by class name.
12
+ -keep class * extends androidx.fragment.app.Fragment { *; }
13
+
14
+ # kotlinx.serialization generates a companion `serializer()` per @Serializable
15
+ # class; the plugin's own rules cover its runtime, these cover our models.
16
+ -keepclassmembers class com.rubyeverywhere.shell.** {
17
+ *** Companion;
18
+ }
19
+ -keepclasseswithmembers class com.rubyeverywhere.shell.** {
20
+ kotlinx.serialization.KSerializer serializer(...);
21
+ }
@@ -0,0 +1,30 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!--
3
+ Debug only. The analogue of iOS's NSAllowsLocalNetworking: enough cleartext
4
+ to reach a Rails server on the developer's machine, and not one host more.
5
+ Never `cleartextTrafficPermitted="true"` on the base config — that would
6
+ quietly downgrade every request the app makes while debugging, including the
7
+ ones going to the real, deployed app.
8
+
9
+ * 127.0.0.1 / localhost — correct on emulators AND physical devices once
10
+ `adb reverse tcp:<port> tcp:<port>` is set up. The preferred rung.
11
+ * 10.0.2.2 — the Android emulator's alias for the host's loopback, used when
12
+ `adb reverse` is unavailable.
13
+ * .local — a Bonjour hostname, which is how most LAN setups are reachable.
14
+
15
+ network-security-config takes hostnames and IP literals only; there is no
16
+ CIDR syntax, so the private ranges cannot be expressed here. An explicit
17
+ `dev_url` of http://192.168.1.20:3000 therefore needs its host added, and
18
+ appending a domain entry to this file for that case is the one edit the
19
+ builder is allowed to make here.
20
+ -->
21
+ <network-security-config>
22
+ <base-config cleartextTrafficPermitted="false" />
23
+
24
+ <domain-config cleartextTrafficPermitted="true">
25
+ <domain includeSubdomains="false">127.0.0.1</domain>
26
+ <domain includeSubdomains="false">localhost</domain>
27
+ <domain includeSubdomains="false">10.0.2.2</domain>
28
+ <domain includeSubdomains="true">local</domain>
29
+ </domain-config>
30
+ </network-security-config>
@@ -0,0 +1,42 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!--
3
+ FROZEN. Nothing per-app belongs in this file. The app label arrives as a
4
+ manifest placeholder from everywhere.properties; deep-link intent filters,
5
+ the auth callback scheme and runtime permissions are merged in from
6
+ src/stamped/AndroidManifest.xml.
7
+ -->
8
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
9
+ xmlns:tools="http://schemas.android.com/tools">
10
+
11
+ <uses-permission android:name="android.permission.INTERNET" />
12
+
13
+ <application
14
+ android:name=".EverywhereApplication"
15
+ android:allowBackup="false"
16
+ android:enableOnBackInvokedCallback="true"
17
+ android:icon="@mipmap/ic_launcher"
18
+ android:label="${everywhereLabel}"
19
+ android:roundIcon="@mipmap/ic_launcher_round"
20
+ android:supportsRtl="true"
21
+ android:theme="@style/Theme.Everywhere"
22
+ android:networkSecurityConfig="@xml/network_security_config"
23
+ tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
24
+
25
+ <activity
26
+ android:name=".MainActivity"
27
+ android:exported="true"
28
+ android:configChanges="orientation|screenSize"
29
+ android:theme="@style/Theme.Everywhere.Splash"
30
+ android:windowSoftInputMode="adjustResize"
31
+ android:launchMode="singleTask">
32
+
33
+ <intent-filter>
34
+ <action android:name="android.intent.action.MAIN" />
35
+ <category android:name="android.intent.category.LAUNCHER" />
36
+ </intent-filter>
37
+
38
+ </activity>
39
+
40
+ </application>
41
+
42
+ </manifest>
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "RubyEverywhere",
3
+ "bundle_id": "com.rubyeverywhere.app",
4
+ "version": "0.1.0",
5
+ "mode": "remote",
6
+ "remote_url": "https://rubyeverywhere.com",
7
+ "entry_path": "/",
8
+ "lazy_load_tabs": true
9
+ }