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.
@@ -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.2.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