forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import UIKit
2import WebKit
3import StoreKit
4
5class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDelegate {
6 let defaults = UserDefaults(suiteName: "group.app.witchsky")
7
8 var window: UIWindow
9 var webView: WKWebView?
10
11 var prevUrl: URL?
12 var starterPackUrl: URL?
13
14 init(window: UIWindow) {
15 self.window = window
16 super.init(nibName: nil, bundle: nil)
17 }
18
19 required init?(coder: NSCoder) {
20 fatalError("init(coder:) has not been implemented")
21 }
22
23 override func viewDidLoad() {
24 super.viewDidLoad()
25
26 let contentController = WKUserContentController()
27 contentController.add(self, name: "onMessage")
28 let configuration = WKWebViewConfiguration()
29 configuration.userContentController = contentController
30
31 let webView = WKWebView(frame: self.view.bounds, configuration: configuration)
32 webView.translatesAutoresizingMaskIntoConstraints = false
33 webView.contentMode = .scaleToFill
34 webView.navigationDelegate = self
35 self.view.addSubview(webView)
36 self.webView = webView
37 self.webView?.load(URLRequest(url: URL(string: "https://bsky.app/?splash=true&clip=true")!))
38 }
39
40 func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
41 guard let response = message.body as? String,
42 let data = response.data(using: .utf8),
43 let payload = try? JSONDecoder().decode(WebViewActionPayload.self, from: data) else {
44 return
45 }
46
47 switch payload.action {
48 case .present:
49 self.presentAppStoreOverlay()
50
51 if let url = self.starterPackUrl {
52 defaults?.setValue(url.absoluteString, forKey: "starterPackUri")
53 }
54 case .store:
55 guard let keyToStoreAs = payload.keyToStoreAs, let jsonToStore = payload.jsonToStore else {
56 return
57 }
58
59 self.defaults?.setValue(jsonToStore, forKey: keyToStoreAs)
60 }
61 }
62
63 func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
64 // Detect when we land on the right URL. This is incase of a short link opening the app clip
65 guard let url = navigationAction.request.url else {
66 return .allow
67 }
68
69 // Store the previous one to compare later, but only set starterPackUrl when we find the right one
70 prevUrl = url
71 // pathComponents starts with "/" as the first component, then each path name. so...
72 // ["/", "start", "name", "rkey"]
73 if isStarterPackUrl(url) {
74 self.starterPackUrl = url
75 }
76
77 return .allow
78 }
79
80 func isStarterPackUrl(_ url: URL) -> Bool {
81 var host: String?
82 if #available(iOS 16.0, *) {
83 host = url.host()
84 } else {
85 host = url.host
86 }
87
88 switch host {
89 case "bsky.app":
90 if url.pathComponents.count == 4,
91 url.pathComponents[1] == "start" || url.pathComponents[1] == "starter-pack" {
92 return true
93 }
94 return false
95 case "go.bsky.app":
96 if url.pathComponents.count == 2 {
97 return true
98 }
99 return false
100 default:
101 return false
102 }
103 }
104
105 func handleURL(url: URL) {
106 if isStarterPackUrl(url) {
107 let urlString = "\(url.absoluteString)?clip=true"
108 if let url = URL(string: urlString) {
109 self.webView?.load(URLRequest(url: url))
110 }
111 } else {
112 self.webView?.load(URLRequest(url: URL(string: "https://bsky.app/?splash=true&clip=true")!))
113 }
114 }
115
116 func presentAppStoreOverlay() {
117 guard let windowScene = self.window.windowScene else {
118 return
119 }
120
121 let configuration = SKOverlay.AppClipConfiguration(position: .bottomRaised)
122 let overlay = SKOverlay(configuration: configuration)
123
124 overlay.present(in: windowScene)
125 }
126
127 func getHost(_ url: URL?) -> String? {
128 if #available(iOS 16.0, *) {
129 return url?.host()
130 } else {
131 return url?.host
132 }
133 }
134
135 func getQuery(_ url: URL?) -> String? {
136 if #available(iOS 16.0, *) {
137 return url?.query()
138 } else {
139 return url?.query
140 }
141 }
142
143 func urlMatchesPrevious(_ url: URL?) -> Bool {
144 if #available(iOS 16.0, *) {
145 return url?.query() == prevUrl?.query() && url?.host() == prevUrl?.host() && url?.query() == prevUrl?.query()
146 } else {
147 return url?.query == prevUrl?.query && url?.host == prevUrl?.host && url?.query == prevUrl?.query
148 }
149 }
150}
151
152struct WebViewActionPayload: Decodable {
153 enum Action: String, Decodable {
154 case present, store
155 }
156
157 let action: Action
158 let keyToStoreAs: String?
159 let jsonToStore: String?
160}