@tokamakdev/platform-ios-simulator-arm64 0.1.1-beta.48 → 0.1.1-beta.50
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.
- package/ios-simulator-arm64/build/entrypoint +13 -30
- package/ios-simulator-arm64/frameworks/TokamakRuntime.framework/Headers/TokamakRuntime.h +10 -0
- package/ios-simulator-arm64/frameworks/TokamakRuntime.framework/TokamakRuntime +0 -0
- package/ios-simulator-arm64/native-shell/TokamakHost.swift +372 -0
- package/ios-simulator-arm64/native-shell/TokamakPlugins.swift +25 -0
- package/ios-simulator-arm64/native-shell/TokamakShell.swift +77 -222
- package/ios-simulator-arm64/platform-pack.json +27 -1
- package/ios-simulator-arm64/tools/tokamak-apple-signing +0 -0
- package/package.json +1 -1
|
@@ -89,36 +89,32 @@ sign_bundle() {
|
|
|
89
89
|
signing_args+=(--device-id "$(cat "$metadata/device-id")")
|
|
90
90
|
fi
|
|
91
91
|
"$apple_tool" "${signing_args[@]}"
|
|
92
|
-
elif [[ "$platform" == macos
|
|
92
|
+
elif [[ "$platform" == macos ]]; then
|
|
93
93
|
"$apple_tool" sign \
|
|
94
94
|
--platform macos \
|
|
95
95
|
--project "$project_dir" \
|
|
96
96
|
--bundle "$bundle" \
|
|
97
97
|
--bundle-id "$identifier"
|
|
98
98
|
else
|
|
99
|
-
# Ad-hoc signatures carry no provisioning profile.
|
|
100
|
-
rm -f "$contents/embedded.provisionprofile"
|
|
101
99
|
codesign --force --timestamp=none --sign - "$bundle"
|
|
102
100
|
fi
|
|
103
101
|
}
|
|
104
102
|
|
|
105
103
|
bundle_key_file=$output_parent/.tokamak/$(basename "$output").input-key
|
|
106
104
|
if [[ -f "$metadata/input-key" ]]; then
|
|
107
|
-
plist_path=
|
|
108
105
|
case "$platform" in
|
|
109
|
-
ios|ios-simulator)
|
|
110
|
-
macos)
|
|
106
|
+
ios|ios-simulator) app_files=("${TOKAMAK_IOS_PLIST:-}" "${TOKAMAK_IOS_ENTITLEMENTS:-}") ;;
|
|
107
|
+
macos) app_files=("${TOKAMAK_MACOS_PLIST:-}" "${TOKAMAK_MACOS_ENTITLEMENTS:-}") ;;
|
|
111
108
|
esac
|
|
112
|
-
if [[ -n "$plist_path" && "$plist_path" != /* ]]; then
|
|
113
|
-
plist_path=$project_dir/$plist_path
|
|
114
|
-
fi
|
|
115
109
|
bundle_key=$(
|
|
116
110
|
{
|
|
117
111
|
cat "$metadata/input-key"
|
|
118
112
|
shasum -a 256 "$0" "$apple_tool"
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
113
|
+
for app_file in "${app_files[@]}"; do
|
|
114
|
+
if [[ -n "$app_file" ]]; then
|
|
115
|
+
shasum -a 256 "$app_file"
|
|
116
|
+
fi
|
|
117
|
+
done
|
|
122
118
|
env | LC_ALL=C sort | sed -n '/^TOKAMAK_/p' |
|
|
123
119
|
sed '/^TOKAMAK_IOS_TEAM_ID=/d; /^TOKAMAK_IOS_SIGNING_IDENTITY=/d; /^TOKAMAK_IOS_PROVISIONING_PROFILE=/d; /^TOKAMAK_MACOS_TEAM_ID=/d; /^TOKAMAK_IOS_BUILD_NUMBER=/d; /^TOKAMAK_MACOS_BUILD_NUMBER=/d'
|
|
124
120
|
printf '%s\n' "${DEVELOPER_DIR:-}" "$PATH"
|
|
@@ -188,11 +184,11 @@ done
|
|
|
188
184
|
registry=$temporary/TokamakPluginRegistry.swift
|
|
189
185
|
{
|
|
190
186
|
printf '%s\n\n' 'import Foundation'
|
|
191
|
-
printf '%s\n' 'func tokamakPlugins() -> [any TokamakPlugin] {'
|
|
187
|
+
printf '%s\n' 'func tokamakPlugins(host: TokamakHost) -> [any TokamakPlugin] {'
|
|
192
188
|
printf '%s\n' ' ['
|
|
193
189
|
for plugin in "$input/plugins"/*; do
|
|
194
190
|
[[ -d "$plugin" ]] || continue
|
|
195
|
-
printf ' %s(),\n' "$(cat "$plugin/class")"
|
|
191
|
+
printf ' %s(host: host),\n' "$(cat "$plugin/class")"
|
|
196
192
|
done
|
|
197
193
|
printf '%s\n' ' ]'
|
|
198
194
|
printf '%s\n' '}'
|
|
@@ -222,24 +218,11 @@ for plugin in "$input/plugins"/*; do
|
|
|
222
218
|
done
|
|
223
219
|
done
|
|
224
220
|
|
|
225
|
-
# Simulator builds embed
|
|
226
|
-
#
|
|
221
|
+
# Simulator builds embed their entitlements in __TEXT,__entitlements, where
|
|
222
|
+
# the simulator reads them.
|
|
227
223
|
if [[ "$platform" == ios-simulator ]]; then
|
|
228
224
|
simulator_entitlements=$temporary/simulator.entitlements
|
|
229
|
-
|
|
230
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
231
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
232
|
-
<plist version="1.0">
|
|
233
|
-
<dict>
|
|
234
|
-
<key>application-identifier</key>
|
|
235
|
-
<string>$identifier</string>
|
|
236
|
-
<key>keychain-access-groups</key>
|
|
237
|
-
<array>
|
|
238
|
-
<string>$identifier</string>
|
|
239
|
-
</array>
|
|
240
|
-
</dict>
|
|
241
|
-
</plist>
|
|
242
|
-
EOF
|
|
225
|
+
"$apple_tool" simulator-entitlements --identifier "$identifier" --output "$simulator_entitlements"
|
|
243
226
|
swift_args+=(-Xlinker -sectcreate -Xlinker __TEXT -Xlinker __entitlements -Xlinker "$simulator_entitlements")
|
|
244
227
|
fi
|
|
245
228
|
|
|
@@ -11,6 +11,12 @@ enum {
|
|
|
11
11
|
TOKAMAK_DECISION_USE = 2,
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
enum {
|
|
15
|
+
TOKAMAK_DISPATCH_HANDLED = 0,
|
|
16
|
+
TOKAMAK_DISPATCH_UNHANDLED = 1,
|
|
17
|
+
TOKAMAK_DISPATCH_FAILED = 2,
|
|
18
|
+
};
|
|
19
|
+
|
|
14
20
|
typedef struct {
|
|
15
21
|
uint8_t *data;
|
|
16
22
|
size_t len;
|
|
@@ -33,6 +39,10 @@ uint16_t tokamak_runtime_restore_gateway(const void *runtime, char *error,
|
|
|
33
39
|
bool tokamak_runtime_suspend(const void *runtime);
|
|
34
40
|
bool tokamak_runtime_resume(const void *runtime);
|
|
35
41
|
void tokamak_runtime_stop(void *runtime);
|
|
42
|
+
int32_t tokamak_runtime_dispatch(const void *runtime, const char *event,
|
|
43
|
+
const char *payload, uint64_t timeout_ms,
|
|
44
|
+
TokamakBytes *result, char *error,
|
|
45
|
+
size_t error_len);
|
|
36
46
|
|
|
37
47
|
int32_t tokamak_runtime_server_authority(const void *runtime, const char *host,
|
|
38
48
|
TokamakBytes *authority);
|
|
Binary file
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import TokamakRuntime
|
|
3
|
+
|
|
4
|
+
private let startupErrorFile = "startup-error.log"
|
|
5
|
+
|
|
6
|
+
/// The tokamak runtime and native plugins of this process. Both outlive any
|
|
7
|
+
/// WebView, so plugins receive app events before a page loads and a Worker
|
|
8
|
+
/// event can run while no page is loaded.
|
|
9
|
+
final class TokamakHost {
|
|
10
|
+
private(set) var plugins: [any TokamakPlugin] = []
|
|
11
|
+
private var runtime: Result<RuntimeHandle, Error>?
|
|
12
|
+
private var waiting: [(Result<RuntimeHandle, Error>) -> Void] = []
|
|
13
|
+
|
|
14
|
+
/// Creates the plugins and starts the runtime in the background. Call it
|
|
15
|
+
/// before launch finishes.
|
|
16
|
+
init() {
|
|
17
|
+
plugins = tokamakPlugins(host: self)
|
|
18
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
19
|
+
let result = Result { try RuntimeHandle() }
|
|
20
|
+
DispatchQueue.main.async { self.started(result) }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Calls `completion` on the main thread once the runtime has started or
|
|
25
|
+
/// failed to start.
|
|
26
|
+
func whenStarted(_ completion: @escaping (Result<RuntimeHandle, Error>) -> Void) {
|
|
27
|
+
dispatchPrecondition(condition: .onQueue(.main))
|
|
28
|
+
if let runtime {
|
|
29
|
+
completion(runtime)
|
|
30
|
+
} else {
|
|
31
|
+
waiting.append(completion)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Runs the Worker's `event` handler with a JSON-serialisable `payload`,
|
|
36
|
+
/// including work it passes to `ctx.waitUntil`, and stops it after
|
|
37
|
+
/// `timeout`. Call it on the main thread; `completion` runs on the main
|
|
38
|
+
/// thread.
|
|
39
|
+
func dispatch(
|
|
40
|
+
event: String,
|
|
41
|
+
payload: Any,
|
|
42
|
+
timeout: TimeInterval,
|
|
43
|
+
completion: @escaping (Result<TokamakEventOutcome, Error>) -> Void
|
|
44
|
+
) {
|
|
45
|
+
whenStarted { result in
|
|
46
|
+
switch result {
|
|
47
|
+
case .failure(let error):
|
|
48
|
+
completion(.failure(error))
|
|
49
|
+
case .success(let runtime):
|
|
50
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
51
|
+
let outcome = Result {
|
|
52
|
+
try runtime.dispatch(event: event, payload: payload, timeout: timeout)
|
|
53
|
+
}
|
|
54
|
+
DispatchQueue.main.async { completion(outcome) }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func didRegisterForRemoteNotifications(deviceToken: Data) {
|
|
61
|
+
for plugin in plugins {
|
|
62
|
+
plugin.didRegisterForRemoteNotifications(deviceToken: deviceToken)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func didFailToRegisterForRemoteNotifications(error: Error) {
|
|
67
|
+
for plugin in plugins {
|
|
68
|
+
plugin.didFailToRegisterForRemoteNotifications(error: error)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Delivers a remote notification to every plugin, calling `completion` on
|
|
73
|
+
/// the main thread once each has finished with it.
|
|
74
|
+
func didReceiveRemoteNotification(
|
|
75
|
+
_ userInfo: [AnyHashable: Any],
|
|
76
|
+
completion: @escaping (TokamakBackgroundResult) -> Void
|
|
77
|
+
) {
|
|
78
|
+
let group = DispatchGroup()
|
|
79
|
+
var results: [TokamakBackgroundResult] = []
|
|
80
|
+
for plugin in plugins {
|
|
81
|
+
group.enter()
|
|
82
|
+
plugin.didReceiveRemoteNotification(userInfo) { result in
|
|
83
|
+
DispatchQueue.main.async {
|
|
84
|
+
results.append(result)
|
|
85
|
+
group.leave()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
group.notify(queue: .main) {
|
|
90
|
+
completion(TokamakBackgroundResult(combining: results))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private func started(_ result: Result<RuntimeHandle, Error>) {
|
|
95
|
+
if case .failure(let error) = result {
|
|
96
|
+
RuntimeHandle.recordStartupFailure(error)
|
|
97
|
+
}
|
|
98
|
+
runtime = result
|
|
99
|
+
let callbacks = waiting
|
|
100
|
+
waiting.removeAll()
|
|
101
|
+
for callback in callbacks {
|
|
102
|
+
callback(result)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// What a Worker event handler did.
|
|
108
|
+
enum TokamakEventOutcome {
|
|
109
|
+
/// The handler's result, decoded from JSON; `NSNull` when it returned nothing.
|
|
110
|
+
case handled(Any)
|
|
111
|
+
/// The Worker has no handler for the event.
|
|
112
|
+
case unhandled
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// What a plugin did with a background event, reported to the system.
|
|
116
|
+
enum TokamakBackgroundResult {
|
|
117
|
+
case newData
|
|
118
|
+
case noData
|
|
119
|
+
case failed
|
|
120
|
+
|
|
121
|
+
/// New data when any plugin fetched some, otherwise a failure when any
|
|
122
|
+
/// plugin failed.
|
|
123
|
+
init(combining results: [Self]) {
|
|
124
|
+
if results.contains(.newData) {
|
|
125
|
+
self = .newData
|
|
126
|
+
} else if results.contains(.failed) {
|
|
127
|
+
self = .failed
|
|
128
|
+
} else {
|
|
129
|
+
self = .noData
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
enum RuntimeError: Error, CustomStringConvertible {
|
|
135
|
+
case configuration(String)
|
|
136
|
+
case runtime(String)
|
|
137
|
+
|
|
138
|
+
var description: String {
|
|
139
|
+
switch self {
|
|
140
|
+
case .configuration(let message), .runtime(let message):
|
|
141
|
+
return message
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
enum AuthenticationMaterial<Value> {
|
|
147
|
+
case defaultHandling
|
|
148
|
+
case cancel
|
|
149
|
+
case use(Value)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// A started tokamak runtime, stopped when released.
|
|
153
|
+
final class RuntimeHandle {
|
|
154
|
+
let appHost: String
|
|
155
|
+
private var handle: UnsafeMutableRawPointer?
|
|
156
|
+
|
|
157
|
+
init() throws {
|
|
158
|
+
guard
|
|
159
|
+
let appHost = Bundle.main.object(forInfoDictionaryKey: "TokamakHost") as? String,
|
|
160
|
+
!appHost.isEmpty
|
|
161
|
+
else {
|
|
162
|
+
throw RuntimeError.configuration("TokamakHost is required")
|
|
163
|
+
}
|
|
164
|
+
self.appHost = appHost
|
|
165
|
+
|
|
166
|
+
let state = try Self.stateDirectory()
|
|
167
|
+
var error = [CChar](repeating: 0, count: 512)
|
|
168
|
+
if let endpoint = Bundle.main.object(forInfoDictionaryKey: "TokamakDevEndpoint") as? String,
|
|
169
|
+
let sessionToken = Bundle.main.object(forInfoDictionaryKey: "TokamakDevSessionToken")
|
|
170
|
+
as? String
|
|
171
|
+
{
|
|
172
|
+
handle = state.path.withCString { statePath in
|
|
173
|
+
appHost.withCString { appHost in
|
|
174
|
+
endpoint.withCString { endpoint in
|
|
175
|
+
sessionToken.withCString { sessionToken in
|
|
176
|
+
error.withUnsafeMutableBufferPointer { error in
|
|
177
|
+
tokamak_runtime_start_development(
|
|
178
|
+
statePath,
|
|
179
|
+
appHost,
|
|
180
|
+
endpoint,
|
|
181
|
+
sessionToken,
|
|
182
|
+
error.baseAddress,
|
|
183
|
+
error.count
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
let bundle = Bundle.main.resourceURL?.appendingPathComponent("app")
|
|
192
|
+
guard let bundle else {
|
|
193
|
+
throw RuntimeError.configuration("app bundle resources are unavailable")
|
|
194
|
+
}
|
|
195
|
+
handle = bundle.path.withCString { bundlePath in
|
|
196
|
+
state.path.withCString { statePath in
|
|
197
|
+
appHost.withCString { appHost in
|
|
198
|
+
error.withUnsafeMutableBufferPointer { error in
|
|
199
|
+
tokamak_runtime_start(
|
|
200
|
+
bundlePath,
|
|
201
|
+
statePath,
|
|
202
|
+
appHost,
|
|
203
|
+
error.baseAddress,
|
|
204
|
+
error.count
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
guard handle != nil else {
|
|
212
|
+
throw RuntimeError.runtime(String(cString: error))
|
|
213
|
+
}
|
|
214
|
+
try? FileManager.default.removeItem(
|
|
215
|
+
at: state.appendingPathComponent(startupErrorFile)
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
deinit {
|
|
220
|
+
if let handle {
|
|
221
|
+
tokamak_runtime_stop(handle)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
var port: UInt16 {
|
|
226
|
+
tokamak_runtime_port(handle)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
func restoreGateway() throws -> UInt16 {
|
|
230
|
+
var error = [CChar](repeating: 0, count: 512)
|
|
231
|
+
let port = error.withUnsafeMutableBufferPointer { error in
|
|
232
|
+
tokamak_runtime_restore_gateway(
|
|
233
|
+
handle,
|
|
234
|
+
error.baseAddress,
|
|
235
|
+
error.count
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
guard port != 0 else {
|
|
239
|
+
throw RuntimeError.runtime(String(cString: error))
|
|
240
|
+
}
|
|
241
|
+
return port
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// Runs the Worker's `event` handler, blocking until it settles or
|
|
245
|
+
/// `timeout` passes.
|
|
246
|
+
func dispatch(event: String, payload: Any, timeout: TimeInterval) throws -> TokamakEventOutcome {
|
|
247
|
+
guard JSONSerialization.isValidJSONObject([payload]) else {
|
|
248
|
+
throw RuntimeError.runtime("the \(event) payload is not JSON")
|
|
249
|
+
}
|
|
250
|
+
let payload = String(
|
|
251
|
+
decoding: try JSONSerialization.data(withJSONObject: payload, options: .fragmentsAllowed),
|
|
252
|
+
as: UTF8.self
|
|
253
|
+
)
|
|
254
|
+
var result = TokamakBytes(data: nil, len: 0)
|
|
255
|
+
var error = [CChar](repeating: 0, count: 1024)
|
|
256
|
+
let status = event.withCString { event in
|
|
257
|
+
payload.withCString { payload in
|
|
258
|
+
error.withUnsafeMutableBufferPointer { error in
|
|
259
|
+
tokamak_runtime_dispatch(
|
|
260
|
+
handle,
|
|
261
|
+
event,
|
|
262
|
+
payload,
|
|
263
|
+
UInt64(timeout * 1000),
|
|
264
|
+
&result,
|
|
265
|
+
error.baseAddress,
|
|
266
|
+
error.count
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
defer { tokamak_bytes_free(result) }
|
|
272
|
+
switch status {
|
|
273
|
+
case Int32(TOKAMAK_DISPATCH_HANDLED):
|
|
274
|
+
guard let bytes = result.data else {
|
|
275
|
+
throw RuntimeError.runtime("the Worker event result is missing")
|
|
276
|
+
}
|
|
277
|
+
return .handled(
|
|
278
|
+
try JSONSerialization.jsonObject(
|
|
279
|
+
with: Data(bytes: bytes, count: result.len),
|
|
280
|
+
options: .fragmentsAllowed
|
|
281
|
+
))
|
|
282
|
+
case Int32(TOKAMAK_DISPATCH_UNHANDLED):
|
|
283
|
+
return .unhandled
|
|
284
|
+
default:
|
|
285
|
+
throw RuntimeError.runtime(String(cString: error))
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
func serverAuthority(
|
|
290
|
+
host: String
|
|
291
|
+
) -> AuthenticationMaterial<Data> {
|
|
292
|
+
var bytes = TokamakBytes(data: nil, len: 0)
|
|
293
|
+
let decision = host.withCString {
|
|
294
|
+
tokamak_runtime_server_authority(handle, $0, &bytes)
|
|
295
|
+
}
|
|
296
|
+
switch decision {
|
|
297
|
+
case Int32(TOKAMAK_DECISION_DEFAULT):
|
|
298
|
+
return .defaultHandling
|
|
299
|
+
case Int32(TOKAMAK_DECISION_USE):
|
|
300
|
+
defer { tokamak_bytes_free(bytes) }
|
|
301
|
+
guard let data = bytes.data else { return .cancel }
|
|
302
|
+
return .use(Data(bytes: data, count: bytes.len))
|
|
303
|
+
default:
|
|
304
|
+
return .cancel
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
func clientIdentity(
|
|
309
|
+
host: String,
|
|
310
|
+
previousFailures: Int
|
|
311
|
+
) -> AuthenticationMaterial<(Data, Data)> {
|
|
312
|
+
var identity = TokamakIdentity(
|
|
313
|
+
certificate: TokamakBytes(data: nil, len: 0),
|
|
314
|
+
private_key: TokamakBytes(data: nil, len: 0)
|
|
315
|
+
)
|
|
316
|
+
let decision = host.withCString {
|
|
317
|
+
tokamak_runtime_client_identity(
|
|
318
|
+
handle,
|
|
319
|
+
$0,
|
|
320
|
+
max(previousFailures, 0),
|
|
321
|
+
&identity
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
switch decision {
|
|
325
|
+
case Int32(TOKAMAK_DECISION_DEFAULT):
|
|
326
|
+
return .defaultHandling
|
|
327
|
+
case Int32(TOKAMAK_DECISION_USE):
|
|
328
|
+
defer { tokamak_identity_free(identity) }
|
|
329
|
+
guard
|
|
330
|
+
let certificate = identity.certificate.data,
|
|
331
|
+
let privateKey = identity.private_key.data
|
|
332
|
+
else {
|
|
333
|
+
return .cancel
|
|
334
|
+
}
|
|
335
|
+
return .use(
|
|
336
|
+
(
|
|
337
|
+
Data(bytes: certificate, count: identity.certificate.len),
|
|
338
|
+
Data(bytes: privateKey, count: identity.private_key.len)
|
|
339
|
+
))
|
|
340
|
+
default:
|
|
341
|
+
return .cancel
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
static func recordStartupFailure(_ error: Error) {
|
|
346
|
+
print("tokamak runtime startup failed: \(error)")
|
|
347
|
+
guard let state = try? stateDirectory() else { return }
|
|
348
|
+
try? String(describing: error).write(
|
|
349
|
+
to: state.appendingPathComponent(startupErrorFile),
|
|
350
|
+
atomically: true,
|
|
351
|
+
encoding: .utf8
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private static func stateDirectory() throws -> URL {
|
|
356
|
+
guard let identifier = Bundle.main.bundleIdentifier else {
|
|
357
|
+
throw RuntimeError.configuration("CFBundleIdentifier is required")
|
|
358
|
+
}
|
|
359
|
+
let root = try FileManager.default.url(
|
|
360
|
+
for: .cachesDirectory,
|
|
361
|
+
in: .userDomainMask,
|
|
362
|
+
appropriateFor: nil,
|
|
363
|
+
create: true
|
|
364
|
+
)
|
|
365
|
+
let state = root.appendingPathComponent(identifier, isDirectory: true)
|
|
366
|
+
try FileManager.default.createDirectory(
|
|
367
|
+
at: state,
|
|
368
|
+
withIntermediateDirectories: true
|
|
369
|
+
)
|
|
370
|
+
return state
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -12,9 +12,12 @@ struct TokamakPluginError: Error {
|
|
|
12
12
|
|
|
13
13
|
typealias TokamakPluginReply = (Result<Any?, TokamakPluginError>) -> Void
|
|
14
14
|
|
|
15
|
+
/// A native plugin, created once per process when the app launches.
|
|
15
16
|
protocol TokamakPlugin: AnyObject {
|
|
16
17
|
var id: String { get }
|
|
17
18
|
|
|
19
|
+
init(host: TokamakHost)
|
|
20
|
+
|
|
18
21
|
func call(
|
|
19
22
|
method: String,
|
|
20
23
|
arguments: Any,
|
|
@@ -26,6 +29,17 @@ protocol TokamakPlugin: AnyObject {
|
|
|
26
29
|
arguments: Any,
|
|
27
30
|
reply: @escaping TokamakPluginReply
|
|
28
31
|
) -> (() -> Void)
|
|
32
|
+
|
|
33
|
+
func didRegisterForRemoteNotifications(deviceToken: Data)
|
|
34
|
+
|
|
35
|
+
func didFailToRegisterForRemoteNotifications(error: Error)
|
|
36
|
+
|
|
37
|
+
/// Handles a remote notification delivered to the app, calling `completion`
|
|
38
|
+
/// once when finished with it.
|
|
39
|
+
func didReceiveRemoteNotification(
|
|
40
|
+
_ userInfo: [AnyHashable: Any],
|
|
41
|
+
completion: @escaping (TokamakBackgroundResult) -> Void
|
|
42
|
+
)
|
|
29
43
|
}
|
|
30
44
|
|
|
31
45
|
extension TokamakPlugin {
|
|
@@ -45,6 +59,17 @@ extension TokamakPlugin {
|
|
|
45
59
|
reply(.failure(.notSupported("\(id).\(method) is not supported")))
|
|
46
60
|
return {}
|
|
47
61
|
}
|
|
62
|
+
|
|
63
|
+
func didRegisterForRemoteNotifications(deviceToken: Data) {}
|
|
64
|
+
|
|
65
|
+
func didFailToRegisterForRemoteNotifications(error: Error) {}
|
|
66
|
+
|
|
67
|
+
func didReceiveRemoteNotification(
|
|
68
|
+
_ userInfo: [AnyHashable: Any],
|
|
69
|
+
completion: @escaping (TokamakBackgroundResult) -> Void
|
|
70
|
+
) {
|
|
71
|
+
completion(.noData)
|
|
72
|
+
}
|
|
48
73
|
}
|
|
49
74
|
|
|
50
75
|
final class TokamakPluginBridge: NSObject, WKScriptMessageHandler {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import Network
|
|
3
3
|
import Security
|
|
4
|
-
import TokamakRuntime
|
|
5
4
|
import WebKit
|
|
6
5
|
|
|
7
6
|
#if os(macOS)
|
|
@@ -10,209 +9,14 @@ import WebKit
|
|
|
10
9
|
import UIKit
|
|
11
10
|
#endif
|
|
12
11
|
|
|
13
|
-
private let startupErrorFile = "startup-error.log"
|
|
14
12
|
private let failurePage =
|
|
15
13
|
"<h1>App failed to start</h1><p>Check startup-error.log for details.</p>"
|
|
16
14
|
|
|
17
|
-
private enum ShellError: Error, CustomStringConvertible {
|
|
18
|
-
case configuration(String)
|
|
19
|
-
case runtime(String)
|
|
20
|
-
|
|
21
|
-
var description: String {
|
|
22
|
-
switch self {
|
|
23
|
-
case .configuration(let message), .runtime(let message):
|
|
24
|
-
return message
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
private enum AuthenticationMaterial<Value> {
|
|
30
|
-
case defaultHandling
|
|
31
|
-
case cancel
|
|
32
|
-
case use(Value)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
private final class RuntimeHost {
|
|
36
|
-
let host: String
|
|
37
|
-
private var handle: UnsafeMutableRawPointer?
|
|
38
|
-
|
|
39
|
-
init() throws {
|
|
40
|
-
guard
|
|
41
|
-
let host = Bundle.main.object(forInfoDictionaryKey: "TokamakHost") as? String,
|
|
42
|
-
!host.isEmpty
|
|
43
|
-
else {
|
|
44
|
-
throw ShellError.configuration("TokamakHost is required")
|
|
45
|
-
}
|
|
46
|
-
self.host = host
|
|
47
|
-
|
|
48
|
-
let state = try Self.stateDirectory()
|
|
49
|
-
var error = [CChar](repeating: 0, count: 512)
|
|
50
|
-
if let endpoint = Bundle.main.object(forInfoDictionaryKey: "TokamakDevEndpoint") as? String,
|
|
51
|
-
let sessionToken = Bundle.main.object(forInfoDictionaryKey: "TokamakDevSessionToken")
|
|
52
|
-
as? String
|
|
53
|
-
{
|
|
54
|
-
handle = state.path.withCString { statePath in
|
|
55
|
-
host.withCString { host in
|
|
56
|
-
endpoint.withCString { endpoint in
|
|
57
|
-
sessionToken.withCString { sessionToken in
|
|
58
|
-
error.withUnsafeMutableBufferPointer { error in
|
|
59
|
-
tokamak_runtime_start_development(
|
|
60
|
-
statePath,
|
|
61
|
-
host,
|
|
62
|
-
endpoint,
|
|
63
|
-
sessionToken,
|
|
64
|
-
error.baseAddress,
|
|
65
|
-
error.count
|
|
66
|
-
)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
} else {
|
|
73
|
-
let bundle = Bundle.main.resourceURL?.appendingPathComponent("app")
|
|
74
|
-
guard let bundle else {
|
|
75
|
-
throw ShellError.configuration("app bundle resources are unavailable")
|
|
76
|
-
}
|
|
77
|
-
handle = bundle.path.withCString { bundlePath in
|
|
78
|
-
state.path.withCString { statePath in
|
|
79
|
-
host.withCString { host in
|
|
80
|
-
error.withUnsafeMutableBufferPointer { error in
|
|
81
|
-
tokamak_runtime_start(
|
|
82
|
-
bundlePath,
|
|
83
|
-
statePath,
|
|
84
|
-
host,
|
|
85
|
-
error.baseAddress,
|
|
86
|
-
error.count
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
guard handle != nil else {
|
|
94
|
-
throw ShellError.runtime(String(cString: error))
|
|
95
|
-
}
|
|
96
|
-
try? FileManager.default.removeItem(
|
|
97
|
-
at: state.appendingPathComponent(startupErrorFile)
|
|
98
|
-
)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
deinit {
|
|
102
|
-
if let handle {
|
|
103
|
-
tokamak_runtime_stop(handle)
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
var port: UInt16 {
|
|
108
|
-
tokamak_runtime_port(handle)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
func restoreGateway() throws -> UInt16 {
|
|
112
|
-
var error = [CChar](repeating: 0, count: 512)
|
|
113
|
-
let port = error.withUnsafeMutableBufferPointer { error in
|
|
114
|
-
tokamak_runtime_restore_gateway(
|
|
115
|
-
handle,
|
|
116
|
-
error.baseAddress,
|
|
117
|
-
error.count
|
|
118
|
-
)
|
|
119
|
-
}
|
|
120
|
-
guard port != 0 else {
|
|
121
|
-
throw ShellError.runtime(String(cString: error))
|
|
122
|
-
}
|
|
123
|
-
return port
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
func serverAuthority(
|
|
127
|
-
host: String
|
|
128
|
-
) -> AuthenticationMaterial<Data> {
|
|
129
|
-
var bytes = TokamakBytes(data: nil, len: 0)
|
|
130
|
-
let decision = host.withCString {
|
|
131
|
-
tokamak_runtime_server_authority(handle, $0, &bytes)
|
|
132
|
-
}
|
|
133
|
-
switch decision {
|
|
134
|
-
case Int32(TOKAMAK_DECISION_DEFAULT):
|
|
135
|
-
return .defaultHandling
|
|
136
|
-
case Int32(TOKAMAK_DECISION_USE):
|
|
137
|
-
defer { tokamak_bytes_free(bytes) }
|
|
138
|
-
guard let data = bytes.data else { return .cancel }
|
|
139
|
-
return .use(Data(bytes: data, count: bytes.len))
|
|
140
|
-
default:
|
|
141
|
-
return .cancel
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
func clientIdentity(
|
|
146
|
-
host: String,
|
|
147
|
-
previousFailures: Int
|
|
148
|
-
) -> AuthenticationMaterial<(Data, Data)> {
|
|
149
|
-
var identity = TokamakIdentity(
|
|
150
|
-
certificate: TokamakBytes(data: nil, len: 0),
|
|
151
|
-
private_key: TokamakBytes(data: nil, len: 0)
|
|
152
|
-
)
|
|
153
|
-
let decision = host.withCString {
|
|
154
|
-
tokamak_runtime_client_identity(
|
|
155
|
-
handle,
|
|
156
|
-
$0,
|
|
157
|
-
max(previousFailures, 0),
|
|
158
|
-
&identity
|
|
159
|
-
)
|
|
160
|
-
}
|
|
161
|
-
switch decision {
|
|
162
|
-
case Int32(TOKAMAK_DECISION_DEFAULT):
|
|
163
|
-
return .defaultHandling
|
|
164
|
-
case Int32(TOKAMAK_DECISION_USE):
|
|
165
|
-
defer { tokamak_identity_free(identity) }
|
|
166
|
-
guard
|
|
167
|
-
let certificate = identity.certificate.data,
|
|
168
|
-
let privateKey = identity.private_key.data
|
|
169
|
-
else {
|
|
170
|
-
return .cancel
|
|
171
|
-
}
|
|
172
|
-
return .use(
|
|
173
|
-
(
|
|
174
|
-
Data(bytes: certificate, count: identity.certificate.len),
|
|
175
|
-
Data(bytes: privateKey, count: identity.private_key.len)
|
|
176
|
-
))
|
|
177
|
-
default:
|
|
178
|
-
return .cancel
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
static func recordStartupFailure(_ error: Error) {
|
|
183
|
-
print("tokamak runtime startup failed: \(error)")
|
|
184
|
-
guard let state = try? stateDirectory() else { return }
|
|
185
|
-
try? String(describing: error).write(
|
|
186
|
-
to: state.appendingPathComponent(startupErrorFile),
|
|
187
|
-
atomically: true,
|
|
188
|
-
encoding: .utf8
|
|
189
|
-
)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
private static func stateDirectory() throws -> URL {
|
|
193
|
-
guard let identifier = Bundle.main.bundleIdentifier else {
|
|
194
|
-
throw ShellError.configuration("CFBundleIdentifier is required")
|
|
195
|
-
}
|
|
196
|
-
let root = try FileManager.default.url(
|
|
197
|
-
for: .cachesDirectory,
|
|
198
|
-
in: .userDomainMask,
|
|
199
|
-
appropriateFor: nil,
|
|
200
|
-
create: true
|
|
201
|
-
)
|
|
202
|
-
let state = root.appendingPathComponent(identifier, isDirectory: true)
|
|
203
|
-
try FileManager.default.createDirectory(
|
|
204
|
-
at: state,
|
|
205
|
-
withIntermediateDirectories: true
|
|
206
|
-
)
|
|
207
|
-
return state
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
15
|
private final class NavigationDelegate: NSObject, WKNavigationDelegate, WKUIDelegate {
|
|
212
|
-
private let runtime:
|
|
16
|
+
private let runtime: RuntimeHandle
|
|
213
17
|
private let pluginBridge: TokamakPluginBridge
|
|
214
18
|
|
|
215
|
-
init(runtime:
|
|
19
|
+
init(runtime: RuntimeHandle, pluginBridge: TokamakPluginBridge) {
|
|
216
20
|
self.runtime = runtime
|
|
217
21
|
self.pluginBridge = pluginBridge
|
|
218
22
|
}
|
|
@@ -239,7 +43,7 @@ private final class NavigationDelegate: NSObject, WKNavigationDelegate, WKUIDele
|
|
|
239
43
|
return
|
|
240
44
|
}
|
|
241
45
|
|
|
242
|
-
if url.isAppOrigin(runtime.
|
|
46
|
+
if url.isAppOrigin(runtime.appHost) {
|
|
243
47
|
if navigationAction.targetFrame == nil {
|
|
244
48
|
webView.load(navigationAction.request)
|
|
245
49
|
decisionHandler(.cancel)
|
|
@@ -266,7 +70,7 @@ private final class NavigationDelegate: NSObject, WKNavigationDelegate, WKUIDele
|
|
|
266
70
|
return
|
|
267
71
|
}
|
|
268
72
|
|
|
269
|
-
if url.isAppOrigin(runtime.
|
|
73
|
+
if url.isAppOrigin(runtime.appHost) {
|
|
270
74
|
decisionHandler(.allow)
|
|
271
75
|
} else {
|
|
272
76
|
openExternal(url)
|
|
@@ -282,7 +86,7 @@ private final class NavigationDelegate: NSObject, WKNavigationDelegate, WKUIDele
|
|
|
282
86
|
) -> WKWebView? {
|
|
283
87
|
_ = (configuration, windowFeatures)
|
|
284
88
|
guard let url = navigationAction.request.url else { return nil }
|
|
285
|
-
if url.isAppOrigin(runtime.
|
|
89
|
+
if url.isAppOrigin(runtime.appHost) {
|
|
286
90
|
webView.load(navigationAction.request)
|
|
287
91
|
} else {
|
|
288
92
|
openExternal(url)
|
|
@@ -299,7 +103,7 @@ private final class NavigationDelegate: NSObject, WKNavigationDelegate, WKUIDele
|
|
|
299
103
|
type: WKMediaCaptureType,
|
|
300
104
|
decisionHandler: @escaping (WKPermissionDecision) -> Void
|
|
301
105
|
) {
|
|
302
|
-
decisionHandler(origin.isAppOrigin(runtime.
|
|
106
|
+
decisionHandler(origin.isAppOrigin(runtime.appHost) ? .grant : .prompt)
|
|
303
107
|
}
|
|
304
108
|
|
|
305
109
|
func webView(
|
|
@@ -457,28 +261,29 @@ private final class NavigationDelegate: NSObject, WKNavigationDelegate, WKUIDele
|
|
|
457
261
|
}
|
|
458
262
|
|
|
459
263
|
private final class TokamakController {
|
|
460
|
-
private
|
|
264
|
+
private let host: TokamakHost
|
|
265
|
+
private var runtime: RuntimeHandle?
|
|
461
266
|
private var navigation: NavigationDelegate?
|
|
462
267
|
private var pluginBridge: TokamakPluginBridge?
|
|
463
268
|
private weak var activeWebView: WKWebView?
|
|
464
269
|
private var dataStore: WKWebsiteDataStore?
|
|
465
270
|
private var proxyPort: UInt16?
|
|
466
271
|
|
|
272
|
+
init(host: TokamakHost) {
|
|
273
|
+
self.host = host
|
|
274
|
+
}
|
|
275
|
+
|
|
467
276
|
func start(
|
|
468
277
|
frame: CGRect,
|
|
469
278
|
completion: @escaping (WKWebView) -> Void
|
|
470
279
|
) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
case .failure(let error):
|
|
479
|
-
RuntimeHost.recordStartupFailure(error)
|
|
480
|
-
completion(self.failureWebView(frame: frame))
|
|
481
|
-
}
|
|
280
|
+
host.whenStarted { result in
|
|
281
|
+
switch result {
|
|
282
|
+
case .success(let runtime):
|
|
283
|
+
self.runtime = runtime
|
|
284
|
+
completion(self.webView(frame: frame, runtime: runtime))
|
|
285
|
+
case .failure:
|
|
286
|
+
completion(self.failureWebView(frame: frame))
|
|
482
287
|
}
|
|
483
288
|
}
|
|
484
289
|
}
|
|
@@ -488,7 +293,7 @@ private final class TokamakController {
|
|
|
488
293
|
do {
|
|
489
294
|
let port = try runtime.restoreGateway()
|
|
490
295
|
guard port != proxyPort else { return }
|
|
491
|
-
setProxy(port: port, host: runtime.
|
|
296
|
+
setProxy(port: port, host: runtime.appHost, dataStore: dataStore)
|
|
492
297
|
proxyPort = port
|
|
493
298
|
activeWebView?.reload()
|
|
494
299
|
} catch {
|
|
@@ -496,11 +301,11 @@ private final class TokamakController {
|
|
|
496
301
|
}
|
|
497
302
|
}
|
|
498
303
|
|
|
499
|
-
private func webView(frame: CGRect, runtime:
|
|
304
|
+
private func webView(frame: CGRect, runtime: RuntimeHandle) -> WKWebView {
|
|
500
305
|
let configuration = WKWebViewConfiguration()
|
|
501
306
|
let dataStore = WKWebsiteDataStore.default()
|
|
502
307
|
let port = runtime.port
|
|
503
|
-
setProxy(port: port, host: runtime.
|
|
308
|
+
setProxy(port: port, host: runtime.appHost, dataStore: dataStore)
|
|
504
309
|
self.dataStore = dataStore
|
|
505
310
|
self.proxyPort = port
|
|
506
311
|
configuration.websiteDataStore = dataStore
|
|
@@ -511,8 +316,8 @@ private final class TokamakController {
|
|
|
511
316
|
configuration.mediaTypesRequiringUserActionForPlayback = []
|
|
512
317
|
|
|
513
318
|
let pluginBridge = TokamakPluginBridge(
|
|
514
|
-
host: runtime.
|
|
515
|
-
plugins:
|
|
319
|
+
host: runtime.appHost,
|
|
320
|
+
plugins: host.plugins
|
|
516
321
|
)
|
|
517
322
|
pluginBridge.install(in: configuration.userContentController)
|
|
518
323
|
let navigation = NavigationDelegate(
|
|
@@ -530,7 +335,7 @@ private final class TokamakController {
|
|
|
530
335
|
self.pluginBridge = pluginBridge
|
|
531
336
|
webView.navigationDelegate = navigation
|
|
532
337
|
webView.uiDelegate = navigation
|
|
533
|
-
webView.load(URLRequest(url: URL(string: "https://\(runtime.
|
|
338
|
+
webView.load(URLRequest(url: URL(string: "https://\(runtime.appHost)/")!))
|
|
534
339
|
return webView
|
|
535
340
|
}
|
|
536
341
|
|
|
@@ -559,7 +364,8 @@ private final class TokamakController {
|
|
|
559
364
|
|
|
560
365
|
#if os(macOS)
|
|
561
366
|
private final class TokamakMacApplicationDelegate: NSObject, NSApplicationDelegate {
|
|
562
|
-
private
|
|
367
|
+
private lazy var host = TokamakHost()
|
|
368
|
+
private lazy var controller = TokamakController(host: host)
|
|
563
369
|
private var window: NSWindow?
|
|
564
370
|
|
|
565
371
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
@@ -595,6 +401,27 @@ private final class TokamakController {
|
|
|
595
401
|
) -> Bool {
|
|
596
402
|
true
|
|
597
403
|
}
|
|
404
|
+
|
|
405
|
+
func application(
|
|
406
|
+
_ application: NSApplication,
|
|
407
|
+
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
|
|
408
|
+
) {
|
|
409
|
+
host.didRegisterForRemoteNotifications(deviceToken: deviceToken)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
func application(
|
|
413
|
+
_ application: NSApplication,
|
|
414
|
+
didFailToRegisterForRemoteNotificationsWithError error: Error
|
|
415
|
+
) {
|
|
416
|
+
host.didFailToRegisterForRemoteNotifications(error: error)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
func application(
|
|
420
|
+
_ application: NSApplication,
|
|
421
|
+
didReceiveRemoteNotification userInfo: [String: Any]
|
|
422
|
+
) {
|
|
423
|
+
host.didReceiveRemoteNotification(userInfo) { _ in }
|
|
424
|
+
}
|
|
598
425
|
}
|
|
599
426
|
|
|
600
427
|
@main
|
|
@@ -609,7 +436,8 @@ private final class TokamakController {
|
|
|
609
436
|
}
|
|
610
437
|
#else
|
|
611
438
|
private final class TokamakIOSApplicationDelegate: UIResponder, UIApplicationDelegate {
|
|
612
|
-
private
|
|
439
|
+
private lazy var host = TokamakHost()
|
|
440
|
+
private lazy var controller = TokamakController(host: host)
|
|
613
441
|
var window: UIWindow?
|
|
614
442
|
|
|
615
443
|
func application(
|
|
@@ -636,6 +464,33 @@ private final class TokamakController {
|
|
|
636
464
|
controller.restoreGateway()
|
|
637
465
|
}
|
|
638
466
|
|
|
467
|
+
func application(
|
|
468
|
+
_ application: UIApplication,
|
|
469
|
+
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
|
|
470
|
+
) {
|
|
471
|
+
host.didRegisterForRemoteNotifications(deviceToken: deviceToken)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
func application(
|
|
475
|
+
_ application: UIApplication,
|
|
476
|
+
didFailToRegisterForRemoteNotificationsWithError error: Error
|
|
477
|
+
) {
|
|
478
|
+
host.didFailToRegisterForRemoteNotifications(error: error)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
func application(
|
|
482
|
+
_ application: UIApplication,
|
|
483
|
+
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
|
|
484
|
+
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
|
|
485
|
+
) {
|
|
486
|
+
host.didReceiveRemoteNotification(userInfo) { result in
|
|
487
|
+
switch result {
|
|
488
|
+
case .newData: completionHandler(.newData)
|
|
489
|
+
case .noData: completionHandler(.noData)
|
|
490
|
+
case .failed: completionHandler(.failed)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
639
494
|
}
|
|
640
495
|
|
|
641
496
|
@main
|
|
@@ -17,5 +17,31 @@
|
|
|
17
17
|
],
|
|
18
18
|
"requiredTools": [
|
|
19
19
|
"xcrun"
|
|
20
|
-
]
|
|
20
|
+
],
|
|
21
|
+
"variables": {
|
|
22
|
+
"build-number": {
|
|
23
|
+
"kind": "string",
|
|
24
|
+
"description": "CFBundleVersion, one to three period-separated integers; defaults to the app version"
|
|
25
|
+
},
|
|
26
|
+
"entitlements": {
|
|
27
|
+
"kind": "path",
|
|
28
|
+
"description": "Entitlements plist layered over the provisioning profile's entitlements at signing"
|
|
29
|
+
},
|
|
30
|
+
"plist": {
|
|
31
|
+
"kind": "path",
|
|
32
|
+
"description": "Info.plist whose values take precedence over generated and plugin values"
|
|
33
|
+
},
|
|
34
|
+
"provisioning-profile": {
|
|
35
|
+
"kind": "path",
|
|
36
|
+
"description": "Provisioning profile for manual signing, used with signing-identity"
|
|
37
|
+
},
|
|
38
|
+
"signing-identity": {
|
|
39
|
+
"kind": "string",
|
|
40
|
+
"description": "SHA-1 of the signing identity for manual signing, used with provisioning-profile"
|
|
41
|
+
},
|
|
42
|
+
"team-id": {
|
|
43
|
+
"kind": "string",
|
|
44
|
+
"description": "Apple Development team for automatic signing"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
21
47
|
}
|
|
Binary file
|