ruby_everywhere 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/bridge/README.md +48 -0
- data/bridge/everywhere/bridge.js +328 -0
- data/bridge/everywhere/native.css +203 -0
- data/bridge/package.json +6 -3
- data/lib/everywhere/builders/ios.rb +32 -0
- data/lib/everywhere/config.rb +104 -1
- data/lib/everywhere/engine.rb +9 -0
- data/lib/everywhere/mobile_config_endpoint.rb +13 -0
- data/lib/everywhere/mobile_configs_controller.rb +18 -0
- data/lib/everywhere/native_helper.rb +196 -0
- data/lib/everywhere/version.rb +1 -1
- data/support/mobile/ios/App/AppDelegate.swift +2 -1
- data/support/mobile/ios/App/Bridge/MenuComponent.swift +192 -0
- data/support/mobile/ios/App/EverywhereConfig.swift +21 -0
- data/support/mobile/ios/App/SceneDelegate.swift +41 -0
- data/support/mobile/ios/App.xcodeproj/project.pbxproj +4 -0
- data/support/mobile/ios/README.md +7 -0
- metadata +3 -1
|
@@ -46,6 +46,7 @@ struct EverywhereConfig: Decodable {
|
|
|
46
46
|
let permissions: [String]?
|
|
47
47
|
let splashMinSeconds: Double?
|
|
48
48
|
let lazyLoadTabs: Bool?
|
|
49
|
+
let universalLinkHosts: [String]?
|
|
49
50
|
|
|
50
51
|
private enum CodingKeys: String, CodingKey {
|
|
51
52
|
case name
|
|
@@ -59,6 +60,7 @@ struct EverywhereConfig: Decodable {
|
|
|
59
60
|
case permissions
|
|
60
61
|
case splashMinSeconds = "splash_min_seconds"
|
|
61
62
|
case lazyLoadTabs = "lazy_load_tabs"
|
|
63
|
+
case universalLinkHosts = "universal_link_hosts"
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
/// Whether the tab bar defers each non-selected tab's initial visit until
|
|
@@ -197,6 +199,25 @@ struct EverywhereConfig: Decodable {
|
|
|
197
199
|
]
|
|
198
200
|
}
|
|
199
201
|
|
|
202
|
+
/// Whether an incoming universal link belongs to this app: https, and its
|
|
203
|
+
/// host is the app's own root host or one of the associated domains
|
|
204
|
+
/// (everywhere.yml deep_linking, injected as `universal_link_hosts`). A link
|
|
205
|
+
/// to some other site is left to Safari.
|
|
206
|
+
func handlesUniversalLink(_ url: URL) -> Bool {
|
|
207
|
+
guard url.scheme?.lowercased() == "https", let host = url.host?.lowercased() else { return false }
|
|
208
|
+
if rootURL.host?.lowercased() == host { return true }
|
|
209
|
+
return (universalLinkHosts ?? []).contains { $0.lowercased() == host }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/// The path (plus query) of an absolute URL, for re-routing a universal link
|
|
213
|
+
/// through the app's own effective root (dev override / picked instance).
|
|
214
|
+
func pathAndQuery(of url: URL) -> String {
|
|
215
|
+
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return "/" }
|
|
216
|
+
let path = components.path.isEmpty ? "/" : components.path
|
|
217
|
+
if let query = components.query, !query.isEmpty { return "\(path)?\(query)" }
|
|
218
|
+
return path
|
|
219
|
+
}
|
|
220
|
+
|
|
200
221
|
/// rootURL joined with an absolute-style path (leading `/` optional).
|
|
201
222
|
func url(forPath path: String) -> URL {
|
|
202
223
|
let root = rootURL
|
|
@@ -39,6 +39,10 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|
|
39
39
|
/// The path the shell treats as the "reset the app" signal.
|
|
40
40
|
private static let resetPath = "/everywhere/reset"
|
|
41
41
|
|
|
42
|
+
/// A universal link that arrived before the root existed (cold launch), held
|
|
43
|
+
/// until the first navigator/tab bar is built, then routed.
|
|
44
|
+
private var pendingUniversalLinkURL: URL?
|
|
45
|
+
|
|
42
46
|
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
|
43
47
|
guard let windowScene = scene as? UIWindowScene else { return }
|
|
44
48
|
|
|
@@ -79,13 +83,50 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|
|
79
83
|
// hydration — the splash covers the wait — and the same pass mirrors
|
|
80
84
|
// cookies for the auth-aware config fetch, which adds the tab bar for
|
|
81
85
|
// a signed-in user.
|
|
86
|
+
// A universal link that launched the app (cold start): capture it now,
|
|
87
|
+
// route it once the root exists (below).
|
|
88
|
+
captureUniversalLink(from: connectionOptions.userActivities)
|
|
89
|
+
|
|
82
90
|
syncWebViewCookiesToSharedStore { [weak self] in
|
|
83
91
|
guard let self else { return }
|
|
84
92
|
self.applyPathConfiguration()
|
|
93
|
+
self.routePendingUniversalLink()
|
|
85
94
|
Hotwire.loadPathConfiguration(from: EverywhereConfig.pathConfigurationSources(serverOnly: true))
|
|
86
95
|
}
|
|
87
96
|
}
|
|
88
97
|
|
|
98
|
+
// MARK: Universal links (deep linking)
|
|
99
|
+
|
|
100
|
+
/// A universal link tapped while the app is running: route it straight to
|
|
101
|
+
/// its in-app path. Links to other sites fall through to the system.
|
|
102
|
+
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
|
|
103
|
+
guard let url = webpageURL(from: userActivity) else { return }
|
|
104
|
+
activeNavigator.route(config.url(forPath: config.pathAndQuery(of: url)))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private func captureUniversalLink(from activities: Set<NSUserActivity>) {
|
|
108
|
+
guard let activity = activities.first(where: { $0.activityType == NSUserActivityTypeBrowsingWeb }),
|
|
109
|
+
let url = webpageURL(from: activity)
|
|
110
|
+
else { return }
|
|
111
|
+
pendingUniversalLinkURL = config.url(forPath: config.pathAndQuery(of: url))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// The webpage URL of a browsing-web activity, but only if this app claims
|
|
115
|
+
/// its host (the associated domains) — never route someone else's link.
|
|
116
|
+
private func webpageURL(from userActivity: NSUserActivity) -> URL? {
|
|
117
|
+
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
|
|
118
|
+
let url = userActivity.webpageURL,
|
|
119
|
+
config.handlesUniversalLink(url)
|
|
120
|
+
else { return nil }
|
|
121
|
+
return url
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private func routePendingUniversalLink() {
|
|
125
|
+
guard let url = pendingUniversalLinkURL else { return }
|
|
126
|
+
pendingUniversalLinkURL = nil
|
|
127
|
+
activeNavigator.route(url)
|
|
128
|
+
}
|
|
129
|
+
|
|
89
130
|
/// Refetch the server path configuration on every foreground — so a config
|
|
90
131
|
/// change lands without a relaunch (the dev endpoint is uncached). Uses the
|
|
91
132
|
/// server-only sources: the auth-aware source of truth, no baked flash.
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
EBE000000000000000000046 /* PermissionsComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBE000000000000000000045 /* PermissionsComponent.swift */; };
|
|
17
17
|
EBE000000000000000000058 /* BiometricsComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBE000000000000000000057 /* BiometricsComponent.swift */; };
|
|
18
18
|
EBE000000000000000000060 /* StorageComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBE000000000000000000059 /* StorageComponent.swift */; };
|
|
19
|
+
EBE000000000000000000068 /* MenuComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBE000000000000000000067 /* MenuComponent.swift */; };
|
|
19
20
|
EBE000000000000000000063 /* EverywhereHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBE000000000000000000062 /* EverywhereHost.swift */; };
|
|
20
21
|
EBE000000000000000000036 /* everywhere.json in Resources */ = {isa = PBXBuildFile; fileRef = EBE000000000000000000018 /* everywhere.json */; };
|
|
21
22
|
EBE000000000000000000037 /* path-configuration.json in Resources */ = {isa = PBXBuildFile; fileRef = EBE000000000000000000019 /* path-configuration.json */; };
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
EBE000000000000000000045 /* PermissionsComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsComponent.swift; sourceTree = "<group>"; };
|
|
39
40
|
EBE000000000000000000057 /* BiometricsComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiometricsComponent.swift; sourceTree = "<group>"; };
|
|
40
41
|
EBE000000000000000000059 /* StorageComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageComponent.swift; sourceTree = "<group>"; };
|
|
42
|
+
EBE000000000000000000067 /* MenuComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuComponent.swift; sourceTree = "<group>"; };
|
|
41
43
|
EBE000000000000000000018 /* everywhere.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = everywhere.json; sourceTree = "<group>"; };
|
|
42
44
|
EBE000000000000000000019 /* path-configuration.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "path-configuration.json"; sourceTree = "<group>"; };
|
|
43
45
|
EBE000000000000000000020 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
|
@@ -105,6 +107,7 @@
|
|
|
105
107
|
EBE000000000000000000045 /* PermissionsComponent.swift */,
|
|
106
108
|
EBE000000000000000000057 /* BiometricsComponent.swift */,
|
|
107
109
|
EBE000000000000000000059 /* StorageComponent.swift */,
|
|
110
|
+
EBE000000000000000000067 /* MenuComponent.swift */,
|
|
108
111
|
);
|
|
109
112
|
path = Bridge;
|
|
110
113
|
sourceTree = "<group>";
|
|
@@ -210,6 +213,7 @@
|
|
|
210
213
|
EBE000000000000000000046 /* PermissionsComponent.swift in Sources */,
|
|
211
214
|
EBE000000000000000000058 /* BiometricsComponent.swift in Sources */,
|
|
212
215
|
EBE000000000000000000060 /* StorageComponent.swift in Sources */,
|
|
216
|
+
EBE000000000000000000068 /* MenuComponent.swift in Sources */,
|
|
213
217
|
EBE000000000000000000063 /* EverywhereHost.swift in Sources */,
|
|
214
218
|
);
|
|
215
219
|
runOnlyForDeploymentPostprocessing = 0;
|
|
@@ -19,6 +19,13 @@ this template, it overwrites **exactly** these files and nothing else:
|
|
|
19
19
|
4. `App/Assets.xcassets/AppIcon.appiconset/AppIcon.png` — the app icon (1024×1024 opaque PNG)
|
|
20
20
|
5. *(optionally)* `App/Assets.xcassets/AccentColor.colorset/Contents.json` and
|
|
21
21
|
`App/Assets.xcassets/LaunchBackground.colorset/Contents.json` — brand colors
|
|
22
|
+
6. *(optionally)* `App/App.entitlements` — the Associated Domains entitlement,
|
|
23
|
+
written **only** when `deep_linking:` is set in `everywhere.yml`. The matching
|
|
24
|
+
`CODE_SIGN_ENTITLEMENTS` is added to the xcconfig in the same case, so the
|
|
25
|
+
default build stays entitlement-free and the frozen template still compiles
|
|
26
|
+
unchanged. The gem serves the `/.well-known/apple-app-site-association` file
|
|
27
|
+
these domains point at; `SceneDelegate` routes an incoming universal link to
|
|
28
|
+
its in-app path.
|
|
22
29
|
|
|
23
30
|
Everything the CLI needs to vary per app flows through those files. The product
|
|
24
31
|
is always `App.app` (`PRODUCT_NAME = App` is hardcoded); identity comes from the
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_everywhere
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrea Fomera
|
|
@@ -79,6 +79,7 @@ files:
|
|
|
79
79
|
- bridge/LICENSE
|
|
80
80
|
- bridge/README.md
|
|
81
81
|
- bridge/everywhere/bridge.js
|
|
82
|
+
- bridge/everywhere/native.css
|
|
82
83
|
- bridge/package.json
|
|
83
84
|
- config/importmap.rb
|
|
84
85
|
- exe/every
|
|
@@ -146,6 +147,7 @@ files:
|
|
|
146
147
|
- support/mobile/ios/App/Base.lproj/LaunchScreen.storyboard
|
|
147
148
|
- support/mobile/ios/App/Bridge/BiometricsComponent.swift
|
|
148
149
|
- support/mobile/ios/App/Bridge/HapticsComponent.swift
|
|
150
|
+
- support/mobile/ios/App/Bridge/MenuComponent.swift
|
|
149
151
|
- support/mobile/ios/App/Bridge/NotificationComponent.swift
|
|
150
152
|
- support/mobile/ios/App/Bridge/PermissionsComponent.swift
|
|
151
153
|
- support/mobile/ios/App/Bridge/StorageComponent.swift
|