vxrn 2.14.2 → 2.15.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vxrn",
3
- "version": "2.14.2",
3
+ "version": "2.15.1",
4
4
  "sideEffects": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -80,14 +80,14 @@
80
80
  "@react-native-community/cli": "^20.2.0",
81
81
  "@react-native-community/cli-config": "^20.2.0",
82
82
  "@react-native/dev-middleware": "^0.88.0",
83
- "@vxrn/compiler": "2.14.2",
84
- "@vxrn/debug": "2.14.2",
85
- "@vxrn/query-string": "2.14.2",
86
- "@vxrn/resolve": "2.14.2",
87
- "@vxrn/url-parse": "2.14.2",
88
- "@vxrn/utils": "2.14.2",
89
- "@vxrn/vendor": "2.14.2",
90
- "@vxrn/vite-flow": "2.14.2",
83
+ "@vxrn/compiler": "2.15.1",
84
+ "@vxrn/debug": "2.15.1",
85
+ "@vxrn/query-string": "2.15.1",
86
+ "@vxrn/resolve": "2.15.1",
87
+ "@vxrn/url-parse": "2.15.1",
88
+ "@vxrn/utils": "2.15.1",
89
+ "@vxrn/vendor": "2.15.1",
90
+ "@vxrn/vite-flow": "2.15.1",
91
91
  "citty": "^0.1.6",
92
92
  "dotenv": "^17.2.1",
93
93
  "dotenv-expand": "^12.0.2",
@@ -112,7 +112,7 @@
112
112
  "devDependencies": {
113
113
  "@biomejs/biome": "2.5.15",
114
114
  "@jridgewell/gen-mapping": "^0.3.5",
115
- "@tamagui/build": "3.2.3",
115
+ "@tamagui/build": "3.3.0",
116
116
  "@types/node": "^24.10.0",
117
117
  "@types/ws": "^8.18.1",
118
118
  "depcheck": "^1.4.7",
@@ -121,20 +121,20 @@
121
121
  "react-native": "0.88.0",
122
122
  "typescript": "7.0.2",
123
123
  "vitest": "^4.1.0",
124
- "@vxrn/vite-plugin-metro": "2.14.2"
124
+ "@vxrn/vite-plugin-metro": "2.15.1"
125
125
  },
126
126
  "publishConfig": {
127
127
  "access": "public"
128
128
  },
129
129
  "peerDependencies": {
130
- "@vxrn/vite-plugin-metro": "2.14.2"
130
+ "@vxrn/vite-plugin-metro": "2.15.1"
131
131
  },
132
132
  "peerDependenciesMeta": {
133
133
  "@vxrn/vite-plugin-metro": {
134
134
  "optional": true
135
135
  }
136
136
  },
137
- "releaseSourceCommit": "16175b9a93e540cad20b5a9c625a904f923e4ddd",
137
+ "releaseSourceCommit": "84d72d51f9619c438190e70050d616a67c5f5bd1",
138
138
  "repository": {
139
139
  "type": "git",
140
140
  "url": "git+https://github.com/onejs/one.git",
@@ -1,4 +1,5 @@
1
1
  import { detectPackageManager, type PackageManagerName } from '@vxrn/utils'
2
+ import { existsSync, lstatSync, readdirSync, readFileSync, renameSync, rmSync } from 'node:fs'
2
3
  import { readFile, writeFile } from 'node:fs/promises'
3
4
  import path from 'node:path'
4
5
  import { fillOptions } from '../config/getOptionsFilled'
@@ -64,6 +65,12 @@ export const prebuild = async ({
64
65
  throw new Error('package.json checks are failing:\n' + error)
65
66
  }
66
67
 
68
+ const ios = path.join(root, 'ios')
69
+ const previousIos = `${ios}.previous`
70
+ rmSync(previousIos, { recursive: true, force: true })
71
+ const reusesIos = (platform == 'ios' || !platform) && existsSync(ios)
72
+ if (reusesIos) renameSync(ios, previousIos)
73
+
67
74
  if (platform == 'ios' || !platform) {
68
75
  await generateForPlatform(root, 'ios', app)
69
76
  }
@@ -73,6 +80,8 @@ export const prebuild = async ({
73
80
 
74
81
  await beforeInstall?.()
75
82
 
83
+ if (reusesIos) reuseUnchangedIosProject(previousIos, ios)
84
+
76
85
  // community config reads the generated projects, so discover after generation
77
86
  // and before installation. `--no-install` never fakes the linked package set.
78
87
  const inventory = await getNativeDependencyInventory(root)
@@ -121,6 +130,27 @@ Android:
121
130
  }
122
131
  }
123
132
 
133
+ // xcode keys each build input on its file identity and modification time, so a
134
+ // regenerated file it already compiled must be the same file, not equal bytes.
135
+ // what pod install and xcodebuild wrote stays too, so both resume incrementally.
136
+ function reuseUnchangedIosProject(previous: string, next: string) {
137
+ for (const entry of readdirSync(next, { recursive: true, withFileTypes: true })) {
138
+ if (!entry.isFile()) continue
139
+ const file = path.join(entry.parentPath, entry.name)
140
+ const old = path.join(previous, path.relative(next, file))
141
+ if (!existsSync(old) || !lstatSync(old).isFile()) continue
142
+ if (lstatSync(old).size !== lstatSync(file).size) continue
143
+ if (!readFileSync(old).equals(readFileSync(file))) continue
144
+ renameSync(old, file)
145
+ }
146
+ for (const name of ['Pods', 'Podfile.lock', 'build']) {
147
+ const from = path.join(previous, name)
148
+ const to = path.join(next, name)
149
+ if (existsSync(from) && !existsSync(to)) renameSync(from, to)
150
+ }
151
+ rmSync(previous, { recursive: true, force: true })
152
+ }
153
+
124
154
  export async function replaceInUTF8File(
125
155
  filePath: string,
126
156
  findThis: string,
@@ -23,6 +23,7 @@ import {
23
23
  generateForPlatform,
24
24
  generateKotlinSources,
25
25
  getNativeDependencyInventory,
26
+ renderIosAppIntentsSwift,
26
27
  renderPrebuildFile,
27
28
  renderSceneDelegateSwift,
28
29
  type PrebuildAppConfig,
@@ -1286,6 +1287,65 @@ describe('ios scene lifecycle', () => {
1286
1287
  expect(sceneDelegate).toContain('RCTLinkingManager.scene(scene, continue: userActivity)')
1287
1288
  })
1288
1289
 
1290
+ it('runs native.app.patches on the generated files and fails on a missing anchor', async () => {
1291
+ const workspaceRoot = fileURLToPath(new URL('../../../..', import.meta.url))
1292
+ const output = mkdtempSync(join(tmpdir(), 'vxrn-prebuild-patches-'))
1293
+ const replaceOnce = (from: string, to: string) => (content: string) => {
1294
+ if (content.includes(to)) return content
1295
+ if (!content.includes(from)) throw new Error(`missing anchor: ${from}`)
1296
+ return content.replace(from, to)
1297
+ }
1298
+ const patches = [
1299
+ {
1300
+ platform: 'ios' as const,
1301
+ file: 'AppDelegate.swift',
1302
+ patch: replaceOnce(
1303
+ 'Bundle.main.url(forResource: "main", withExtension: "jsbundle")',
1304
+ 'HotUpdater.bundleURL()'
1305
+ ),
1306
+ },
1307
+ {
1308
+ platform: 'ios' as const,
1309
+ file: 'MyApp/Info.plist',
1310
+ patch: replaceOnce('<dict>', '<dict>\n\t<key>HotUpdaterChannel</key>\n\t<string>production</string>'),
1311
+ },
1312
+ {
1313
+ platform: 'android' as const,
1314
+ file: 'MainApplication.kt',
1315
+ patch: replaceOnce(
1316
+ 'getDefaultReactHost(\n context = applicationContext,',
1317
+ 'getDefaultReactHost(\n context = applicationContext,\n jsBundleFilePath = HotUpdater.getJSBundleFile(applicationContext),'
1318
+ ),
1319
+ },
1320
+ ]
1321
+ await generateForPlatform(workspaceRoot, 'ios', { ...app, patches }, join(output, 'ios'))
1322
+ await generateForPlatform(workspaceRoot, 'android', { ...app, patches }, join(output, 'android'))
1323
+ expect(readFileSync(join(output, 'ios', 'MyApp', 'AppDelegate.swift'), 'utf8')).toContain(
1324
+ 'HotUpdater.bundleURL()'
1325
+ )
1326
+ expect(readFileSync(join(output, 'ios', 'MyApp', 'Info.plist'), 'utf8')).toContain(
1327
+ 'HotUpdaterChannel'
1328
+ )
1329
+ expect(
1330
+ readFileSync(
1331
+ join(output, 'android', 'app/src/main/java/dev/one/myapp/MainApplication.kt'),
1332
+ 'utf8'
1333
+ )
1334
+ ).toContain('jsBundleFilePath = HotUpdater.getJSBundleFile(applicationContext)')
1335
+
1336
+ await expect(
1337
+ generateForPlatform(
1338
+ workspaceRoot,
1339
+ 'ios',
1340
+ {
1341
+ ...app,
1342
+ patches: [{ platform: 'ios', file: 'AppDelegate.swift', patch: replaceOnce('nope-anchor', 'replacement-text') }],
1343
+ },
1344
+ join(output, 'ios2')
1345
+ )
1346
+ ).rejects.toThrow('missing anchor')
1347
+ })
1348
+
1289
1349
  it('bundles native.app fonts into both platforms from the real template', async () => {
1290
1350
  const workspaceRoot = fileURLToPath(new URL('../../../..', import.meta.url))
1291
1351
  const output = mkdtempSync(join(tmpdir(), 'vxrn-prebuild-fonts-'))
@@ -2139,9 +2199,12 @@ describe('ios widgets', () => {
2139
2199
  'rectangle',
2140
2200
  'rounded-rectangle',
2141
2201
  'link',
2202
+ 'button',
2142
2203
  ]) {
2143
2204
  expect(rendered).toContain(`case "${type}":`)
2144
2205
  }
2206
+ expect(rendered).toContain('struct OneWidgetButtonIntent: AppIntent')
2207
+ expect(rendered).toContain('.supportedFamilies([.systemSmall, .systemMedium])')
2145
2208
  // every ActivityView slot needs a decode path in both presentations
2146
2209
  for (const slot of [
2147
2210
  'lockScreen',
@@ -2155,13 +2218,81 @@ describe('ios widgets', () => {
2155
2218
  expect(rendered).toContain(slot)
2156
2219
  }
2157
2220
  // the JSX bridge methods exist on both sides of the React Native bridge
2158
- for (const method of ['writeView', 'startView', 'updateView']) {
2221
+ for (const method of ['writeView', 'startView', 'updateView', 'claimActions']) {
2159
2222
  expect(readGenerated('MyApp/OneWidgetsBridge.swift')).toContain(method)
2160
2223
  expect(readGenerated('MyApp/OneWidgetsBridge.m')).toContain(method)
2161
2224
  }
2162
- // the shared contract carries the serialized layouts
2225
+ expect(readGenerated('MyApp/OneWidgetsBridge.swift')).toContain('"oneWidgetAction"')
2226
+ // the shared contract carries the serialized layouts and interactive actions
2163
2227
  const contract = readGenerated('MyApp/OneWidgetContract.swift')
2164
2228
  expect(contract.match(/let layout: String\?/g)).toHaveLength(2)
2229
+ expect(contract).toContain('static let actionKeyPrefix = "one.widget.action."')
2230
+ expect(contract).toContain('struct Action: Codable')
2231
+ expect(rendered).toContain('OneWidgetContract.actionKeyPrefix + item.id')
2232
+ expect(rendered).toContain('OneWidgetButtonIntent(action: action, value: node.value?.string)')
2233
+ expect(readGenerated('MyApp/OneWidgetsBridge.swift')).toContain(
2234
+ 'key.hasPrefix(OneWidgetContract.actionKeyPrefix)'
2235
+ )
2236
+ }, 180000)
2237
+
2238
+ it('emits configured widget families and App Intents spoken dialog metadata', async () => {
2239
+ const workspaceRoot = fileURLToPath(new URL('../../../..', import.meta.url))
2240
+ const output = mkdtempSync(join(tmpdir(), 'vxrn-prebuild-widgets-families-'))
2241
+ await generateForPlatform(
2242
+ workspaceRoot,
2243
+ 'ios',
2244
+ {
2245
+ ...widgetsApp,
2246
+ ios: {
2247
+ ...widgetsApp.ios,
2248
+ widgets: {
2249
+ ...widgetsApp.ios.widgets,
2250
+ families: ['systemSmall', 'systemLarge', 'accessoryCircular', 'accessoryRectangular'],
2251
+ },
2252
+ },
2253
+ },
2254
+ join(output, 'ios')
2255
+ )
2256
+ const renderedWidget = readFileSync(
2257
+ join(output, 'ios', 'OneWidgets', 'OneWidget.swift'),
2258
+ 'utf8'
2259
+ )
2260
+ expect(renderedWidget).toContain(
2261
+ '.supportedFamilies([.systemSmall, .systemLarge, .accessoryCircular, .accessoryRectangular])'
2262
+ )
2263
+
2264
+ const swift = renderIosAppIntentsSwift([
2265
+ {
2266
+ id: 'dev.one.myapp.ask',
2267
+ title: 'Ask assistant',
2268
+ description: 'Run a voice query and speak the answer.',
2269
+ textParameterTitle: 'Prompt',
2270
+ shortcutPhrase: 'Ask {app} a question',
2271
+ shortcutPhrases: ['Query {app}', 'Ask {app} a question'],
2272
+ systemImageName: 'waveform.circle.fill',
2273
+ openAppWhenRun: true,
2274
+ },
2275
+ {
2276
+ id: 'dev.one.myapp.ping',
2277
+ title: 'Ping',
2278
+ shortcutPhrase: 'Ping {app}',
2279
+ },
2280
+ ])
2281
+ expect(swift).toContain(
2282
+ 'func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog'
2283
+ )
2284
+ expect(swift).toContain(
2285
+ 'return .result(value: result, dialog: IntentDialog(stringLiteral: result))'
2286
+ )
2287
+ expect(swift).toContain(
2288
+ 'static let description = IntentDescription("Run a voice query and speak the answer.")'
2289
+ )
2290
+ expect(swift).toContain('static let openAppWhenRun: Bool = true')
2291
+ expect(swift).toContain(
2292
+ 'phrases: ["Ask \\(.applicationName) a question", "Query \\(.applicationName)"]'
2293
+ )
2294
+ expect(swift).toContain('systemImageName: "waveform.circle.fill"')
2295
+ expect(swift).toContain('systemImageName: "bolt.fill"')
2165
2296
  }, 180000)
2166
2297
 
2167
2298
  it('regenerates byte-identical widget projects and adds nothing without the config', async () => {
@@ -372,6 +372,12 @@ function swiftShortcutPhrase(value: string): string {
372
372
  export function renderIosAppIntentsSwift(actions: NonNullable<NonNullable<NativeAppManifest['ios']>['appIntents']>['actions']): string {
373
373
  const types = actions.map((action, index) => {
374
374
  const summaryTitle = swiftLiteral(action.title).slice(1, -1)
375
+ const description =
376
+ action.description === undefined
377
+ ? ''
378
+ : `\n static let description = IntentDescription(${swiftLiteral(action.description)})`
379
+ const openAppWhenRun =
380
+ action.openAppWhenRun === true ? '\n static let openAppWhenRun: Bool = true' : ''
375
381
  const text = action.textParameterTitle === undefined ? '' : `
376
382
  @Parameter(title: ${swiftLiteral(action.textParameterTitle)}) var text: String
377
383
 
@@ -380,20 +386,25 @@ export function renderIosAppIntentsSwift(actions: NonNullable<NonNullable<Native
380
386
  }
381
387
  `
382
388
  return `struct OneAppIntent${index}: AppIntent {
383
- static let title: LocalizedStringResource = ${swiftLiteral(action.title)}
389
+ static let title: LocalizedStringResource = ${swiftLiteral(action.title)}${description}${openAppWhenRun}
384
390
  ${text}
385
- func perform() async throws -> some IntentResult & ReturnsValue<String> {
391
+ func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog {
386
392
  let result = try await oneAppIntentsRun(${swiftLiteral(action.id)}, ${text ? 'text' : 'nil'})
387
- return .result(value: result)
393
+ return .result(value: result, dialog: IntentDialog(stringLiteral: result))
388
394
  }
389
395
  }`
390
396
  })
391
- const shortcuts = actions.map((action, index) => ` AppShortcut(
397
+ const shortcuts = actions.map((action, index) => {
398
+ const phrases = Array.from(
399
+ new Set([action.shortcutPhrase, ...(action.shortcutPhrases ?? [])])
400
+ ).map(swiftShortcutPhrase)
401
+ return ` AppShortcut(
392
402
  intent: OneAppIntent${index}(),
393
- phrases: [${swiftShortcutPhrase(action.shortcutPhrase)}],
403
+ phrases: [${phrases.join(', ')}],
394
404
  shortTitle: ${swiftLiteral(action.title)},
395
- systemImageName: "bolt.fill"
396
- )`).join('\n')
405
+ systemImageName: ${swiftLiteral(action.systemImageName ?? 'bolt.fill')}
406
+ )`
407
+ }).join('\n')
397
408
  return `import AppIntents
398
409
  import Foundation
399
410
 
@@ -871,6 +882,9 @@ function generateIosWidgets(dest: string, app: NativeAppManifest): void {
871
882
  </dict></plist>
872
883
  `
873
884
  )
885
+ const supportedFamilies = (widgets.families ?? ['systemSmall', 'systemMedium'])
886
+ .map((family) => `.${family}`)
887
+ .join(', ')
874
888
  writeFileSync(
875
889
  path.join(appDir, 'OneWidgetContract.swift'),
876
890
  `import Foundation
@@ -880,6 +894,7 @@ enum OneWidgetContract {
880
894
  static let appGroup = ${JSON.stringify(widgets.appGroup)}
881
895
  static let kind = ${JSON.stringify(widgets.kind)}
882
896
  static let dataKey = "one.widget.data"
897
+ static let actionKeyPrefix = "one.widget.action."
883
898
 
884
899
  struct Data: Codable {
885
900
  let title: String
@@ -888,6 +903,13 @@ enum OneWidgetContract {
888
903
  let layout: String?
889
904
  }
890
905
 
906
+ struct Action: Codable {
907
+ let id: String
908
+ let action: String
909
+ let value: String?
910
+ let timestampMs: Double
911
+ }
912
+
891
913
  struct Attributes: ActivityAttributes {
892
914
  struct ContentState: Codable, Hashable {
893
915
  let status: String
@@ -904,6 +926,7 @@ enum OneWidgetContract {
904
926
  `import SwiftUI
905
927
  import WidgetKit
906
928
  import ActivityKit
929
+ import AppIntents
907
930
 
908
931
  struct OneWidgetNode: Decodable {
909
932
  let type: String
@@ -911,11 +934,36 @@ struct OneWidgetNode: Decodable {
911
934
  let style: Style?
912
935
  let children: [OneWidgetNode]?
913
936
  let systemName: String?
914
- let value: Double?
937
+ let value: Value?
915
938
  let total: Double?
916
939
  let fill: String?
917
940
  let cornerRadius: Double?
918
941
  let url: String?
942
+ let action: String?
943
+
944
+ enum Value: Decodable {
945
+ case number(Double)
946
+ case string(String)
947
+
948
+ var number: Double? {
949
+ if case .number(let value) = self { return value }
950
+ return nil
951
+ }
952
+
953
+ var string: String? {
954
+ if case .string(let value) = self { return value }
955
+ return nil
956
+ }
957
+
958
+ init(from decoder: Decoder) throws {
959
+ let container = try decoder.singleValueContainer()
960
+ if let number = try? container.decode(Double.self) {
961
+ self = .number(number)
962
+ } else {
963
+ self = .string(try container.decode(String.self))
964
+ }
965
+ }
966
+ }
919
967
 
920
968
  struct Style: Decodable {
921
969
  let color: String?
@@ -934,6 +982,43 @@ struct OneWidgetNode: Decodable {
934
982
  }
935
983
  }
936
984
 
985
+ struct OneWidgetButtonIntent: AppIntent {
986
+ static var title: LocalizedStringResource = "Widget Action"
987
+
988
+ @Parameter(title: "Action") var action: String
989
+ @Parameter(title: "Value") var value: String?
990
+
991
+ init() {}
992
+
993
+ init(action: String, value: String?) {
994
+ self.action = action
995
+ self.value = value
996
+ }
997
+
998
+ func perform() async throws -> some IntentResult {
999
+ if let defaults = UserDefaults(suiteName: OneWidgetContract.appGroup) {
1000
+ let item = OneWidgetContract.Action(
1001
+ id: UUID().uuidString,
1002
+ action: action,
1003
+ value: value,
1004
+ timestampMs: Date().timeIntervalSince1970 * 1000
1005
+ )
1006
+ if let encoded = try? JSONEncoder().encode(item) {
1007
+ defaults.set(encoded, forKey: OneWidgetContract.actionKeyPrefix + item.id)
1008
+ }
1009
+ CFNotificationCenterPostNotification(
1010
+ CFNotificationCenterGetDarwinNotifyCenter(),
1011
+ CFNotificationName("dev.onejs.widget.action" as CFString),
1012
+ nil,
1013
+ nil,
1014
+ true
1015
+ )
1016
+ }
1017
+ WidgetCenter.shared.reloadTimelines(ofKind: OneWidgetContract.kind)
1018
+ return .result()
1019
+ }
1020
+ }
1021
+
937
1022
  struct OneActivityView: Decodable {
938
1023
  let lockScreen: OneWidgetNode
939
1024
  let compactLeading: OneWidgetNode?
@@ -1026,10 +1111,10 @@ struct OneWidgetRendered: View {
1026
1111
  .font(.system(size: CGFloat(node.style?.fontSize ?? 20), weight: weight))
1027
1112
  }
1028
1113
  case "progress":
1029
- ProgressView(value: node.value ?? 0, total: node.total ?? 1)
1114
+ ProgressView(value: node.value?.number ?? 0, total: node.total ?? 1)
1030
1115
  .tint(oneColor(node.style?.color))
1031
1116
  case "gauge":
1032
- Gauge(value: node.value ?? 0, in: 0...(node.total ?? 1)) { EmptyView() }
1117
+ Gauge(value: node.value?.number ?? 0, in: 0...(node.total ?? 1)) { EmptyView() }
1033
1118
  .gaugeStyle(.accessoryCircular)
1034
1119
  .tint(oneColor(node.style?.color))
1035
1120
  case "circle":
@@ -1047,6 +1132,15 @@ struct OneWidgetRendered: View {
1047
1132
  }
1048
1133
  }
1049
1134
  }
1135
+ case "button":
1136
+ if let action = node.action {
1137
+ Button(intent: OneWidgetButtonIntent(action: action, value: node.value?.string)) {
1138
+ ForEach(Array((node.children ?? []).enumerated()), id: \\.offset) { _, child in
1139
+ OneWidgetRendered(node: child)
1140
+ }
1141
+ }
1142
+ .buttonStyle(.plain)
1143
+ }
1050
1144
  default: EmptyView()
1051
1145
  }
1052
1146
  }
@@ -1107,7 +1201,7 @@ struct OneWidget: Widget {
1107
1201
  }
1108
1202
  .configurationDisplayName(${JSON.stringify(widgets.displayName)})
1109
1203
  .description(${JSON.stringify(widgets.description)})
1110
- .supportedFamilies([.systemSmall, .systemMedium])
1204
+ .supportedFamilies([${supportedFamilies}])
1111
1205
  }
1112
1206
  }
1113
1207
 
@@ -1204,31 +1298,120 @@ RCT_EXTERN_METHOD(update:(NSString *)identifier status:(NSString *)status value:
1204
1298
  RCT_EXTERN_METHOD(updateView:(NSString *)identifier layout:(NSString *)layout resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
1205
1299
  RCT_EXTERN_METHOD(end:(NSString *)identifier resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
1206
1300
  RCT_EXTERN_METHOD(pushToken:(NSString *)identifier resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
1301
+ RCT_EXTERN_METHOD(claimActions:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
1207
1302
  @end
1208
1303
  `
1209
1304
  )
1210
1305
  writeFileSync(
1211
1306
  path.join(appDir, 'OneWidgetsBridge.swift'),
1212
1307
  `import Foundation
1308
+ import UIKit
1213
1309
  import React
1214
1310
  import WidgetKit
1215
1311
  import ActivityKit
1216
1312
 
1313
+ private func oneWidgetsDarwinCallback(
1314
+ _ center: CFNotificationCenter?,
1315
+ _ observer: UnsafeMutableRawPointer?,
1316
+ _ name: CFNotificationName?,
1317
+ _ object: UnsafeRawPointer?,
1318
+ _ userInfo: CFDictionary?
1319
+ ) {
1320
+ guard let observer else { return }
1321
+ let bridge = Unmanaged<OneWidgetsBridge>.fromOpaque(observer).takeUnretainedValue()
1322
+ bridge.flushObservedActions()
1323
+ }
1324
+
1217
1325
  @objc(OneWidgetsBridge)
1218
1326
  class OneWidgetsBridge: RCTEventEmitter {
1219
1327
  private var tokenTasks: [String: Task<Void, Never>] = [:]
1328
+ private var isObservingEvents = false
1329
+ private var foregroundObserver: NSObjectProtocol?
1330
+ private let actionLock = NSLock()
1220
1331
 
1221
- override func supportedEvents() -> [String]! { ["oneLiveActivityPushToken"] }
1332
+ override func supportedEvents() -> [String]! { ["oneLiveActivityPushToken", "oneWidgetAction"] }
1222
1333
 
1223
1334
  override func startObserving() {
1335
+ isObservingEvents = true
1224
1336
  for activity in Activity<OneWidgetContract.Attributes>.activities {
1225
1337
  observeToken(activity)
1226
1338
  }
1339
+ CFNotificationCenterAddObserver(
1340
+ CFNotificationCenterGetDarwinNotifyCenter(),
1341
+ Unmanaged.passUnretained(self).toOpaque(),
1342
+ oneWidgetsDarwinCallback,
1343
+ "dev.onejs.widget.action" as CFString,
1344
+ nil,
1345
+ .deliverImmediately
1346
+ )
1347
+ foregroundObserver = NotificationCenter.default.addObserver(
1348
+ forName: UIApplication.didBecomeActiveNotification,
1349
+ object: nil,
1350
+ queue: .main
1351
+ ) { [weak self] _ in
1352
+ self?.flushObservedActions()
1353
+ }
1354
+ flushObservedActions()
1227
1355
  }
1228
1356
 
1229
1357
  override func stopObserving() {
1358
+ isObservingEvents = false
1230
1359
  for task in tokenTasks.values { task.cancel() }
1231
1360
  tokenTasks.removeAll()
1361
+ CFNotificationCenterRemoveObserver(
1362
+ CFNotificationCenterGetDarwinNotifyCenter(),
1363
+ Unmanaged.passUnretained(self).toOpaque(),
1364
+ CFNotificationName("dev.onejs.widget.action" as CFString),
1365
+ nil
1366
+ )
1367
+ if let foregroundObserver {
1368
+ NotificationCenter.default.removeObserver(foregroundObserver)
1369
+ self.foregroundObserver = nil
1370
+ }
1371
+ }
1372
+
1373
+ deinit {
1374
+ CFNotificationCenterRemoveObserver(
1375
+ CFNotificationCenterGetDarwinNotifyCenter(),
1376
+ Unmanaged.passUnretained(self).toOpaque(),
1377
+ CFNotificationName("dev.onejs.widget.action" as CFString),
1378
+ nil
1379
+ )
1380
+ if let foregroundObserver {
1381
+ NotificationCenter.default.removeObserver(foregroundObserver)
1382
+ }
1383
+ }
1384
+
1385
+ fileprivate func flushObservedActions() {
1386
+ guard isObservingEvents else { return }
1387
+ for item in drainStoredActions() {
1388
+ sendEvent(withName: "oneWidgetAction", body: item)
1389
+ }
1390
+ }
1391
+
1392
+ private func drainStoredActions() -> [[String: Any]] {
1393
+ actionLock.lock()
1394
+ defer { actionLock.unlock() }
1395
+ guard let defaults = UserDefaults(suiteName: OneWidgetContract.appGroup) else {
1396
+ return []
1397
+ }
1398
+ var actions: [OneWidgetContract.Action] = []
1399
+ for key in defaults.dictionaryRepresentation().keys where key.hasPrefix(OneWidgetContract.actionKeyPrefix) {
1400
+ guard let data = defaults.data(forKey: key) else { continue }
1401
+ defaults.removeObject(forKey: key)
1402
+ if let action = try? JSONDecoder().decode(OneWidgetContract.Action.self, from: data) {
1403
+ actions.append(action)
1404
+ }
1405
+ }
1406
+ actions.sort { $0.timestampMs < $1.timestampMs }
1407
+ return actions.map { action in
1408
+ [
1409
+ "id": action.id,
1410
+ "action": action.action,
1411
+ "value": action.value ?? NSNull(),
1412
+ "timestampMs": action.timestampMs,
1413
+ ]
1414
+ }
1232
1415
  }
1233
1416
 
1234
1417
  private func observeToken(_ activity: Activity<OneWidgetContract.Attributes>) {
@@ -1370,6 +1553,15 @@ class OneWidgetsBridge: RCTEventEmitter {
1370
1553
  resolve(NSNull())
1371
1554
  }
1372
1555
  }
1556
+
1557
+ @objc(claimActions:rejecter:)
1558
+ func claimActions(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
1559
+ guard UserDefaults(suiteName: OneWidgetContract.appGroup) != nil else {
1560
+ reject("app_group", "Cannot open the configured App Group", nil)
1561
+ return
1562
+ }
1563
+ resolve(drainStoredActions())
1564
+ }
1373
1565
  }
1374
1566
  `
1375
1567
  )
@@ -2972,6 +3164,36 @@ export const generateForPlatform = async (
2972
3164
  if (platform === 'ios') await generateSwiftPackages({ root, dest })
2973
3165
  if (platform === 'android') generateAndroidIconForwarder(dest, app)
2974
3166
  if (platform === 'android') await generateKotlinSources({ root, dest })
3167
+ applyAppPatches(dest, platform, app)
3168
+ }
3169
+
3170
+ function listFiles(dir: string): string[] {
3171
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) =>
3172
+ entry.isDirectory() ? listFiles(path.join(dir, entry.name)) : [path.join(dir, entry.name)]
3173
+ )
3174
+ }
3175
+
3176
+ function applyAppPatches(dest: string, platform: 'ios' | 'android', app: NativeAppManifest) {
3177
+ const patches = (app.patches ?? []).filter((entry) => entry.platform === platform)
3178
+ if (patches.length === 0) return
3179
+ const files = listFiles(dest)
3180
+ for (const { file, patch } of patches) {
3181
+ const matches = files.filter((candidate) => {
3182
+ const relative = path.relative(dest, candidate).split(path.sep).join('/')
3183
+ return relative === file || relative.endsWith('/' + file)
3184
+ })
3185
+ if (matches.length !== 1) {
3186
+ throw new Error(
3187
+ `[vxrn] native.app.patches: "${file}" matched ${matches.length} ${platform} files, expected exactly one`
3188
+ )
3189
+ }
3190
+ const before = readFileSync(matches[0], 'utf8')
3191
+ const after = patch(before)
3192
+ if (patch(after) !== after) {
3193
+ throw new Error(`[vxrn] native.app.patches: patch for "${file}" is not idempotent`)
3194
+ }
3195
+ writeFileSync(matches[0], after)
3196
+ }
2975
3197
  }
2976
3198
 
2977
3199
  // an imported kotlin source can reach past the base ui/foundation/material3
@@ -1 +1 @@
1
- {"version":3,"file":"prebuild.d.ts","sourceRoot":"","sources":["../../src/exports/prebuild.ts"],"names":[],"mappings":"AAKA,OAAO,EAML,KAAK,iBAAiB,EACvB,MAAM,uBAAuB,CAAA;AAK9B,eAAO,MAAM,QAAQ,qEAMlB;IACD,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,MAAM,CAAA;IACrC,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,GAAG,EAAE,iBAAiB,CAAA;IAGtB,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACpC,kBA0FA,CAAA;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,iBAOpB"}
1
+ {"version":3,"file":"prebuild.d.ts","sourceRoot":"","sources":["../../src/exports/prebuild.ts"],"names":[],"mappings":"AAMA,OAAO,EAML,KAAK,iBAAiB,EACvB,MAAM,uBAAuB,CAAA;AAK9B,eAAO,MAAM,QAAQ,qEAMlB;IACD,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,MAAM,CAAA;IACrC,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,GAAG,EAAE,iBAAiB,CAAA;IAGtB,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACpC,kBAkGA,CAAA;AAuBD,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,iBAOpB"}
@@ -1 +1 @@
1
- {"version":3,"file":"prebuildWithoutExpo.d.ts","sourceRoot":"","sources":["../../src/exports/prebuildWithoutExpo.ts"],"names":[],"mappings":"AAkBA,OAAO,EAKL,iBAAiB,EAEjB,KAAK,iBAAiB,EAEvB,MAAM,+BAA+B,CAAA;AAwEtC,YAAY,EAAE,iBAAiB,IAAI,iBAAiB,EAAE,MAAM,+BAA+B,CAAA;AAE3F,eAAO,MAAM,mBAAmB,0BAAoB,CAAA;AA6EpD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAwChE;AAyJD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,WAAW,CAAC,WAAW,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,MAAM,CA8CrI;AAg8BD,MAAM,WAAW,oBAAoB;IACnC,gBAAgB,EAAE,MAAM,CAAA;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AA6sBD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,EAAE,KAAK,GAAG,SAAS,CAAA;IAC3B,GAAG,EAAE,iBAAiB,CAAA;IACtB,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB,GAAG,oBAAoB,CA0xBvB;AAED,eAAO,MAAM,mBAAmB,SACxB,MAAM,YACF,KAAK,GAAG,SAAS,OACtB,iBAAiB,WACd,MAAM,kBAuEf,CAAA;AAgCD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,QA6EvD;AAID,wBAAsB,qBAAqB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,iBAsDzF;AAyFD,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAClD,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,iBAAiB,CAAA;IACtB,SAAS,EAAE,SAAS,yBAAyB,EAAE,CAAA;CAChD,GAAG,IAAI,CAmCP;AAID,wBAAsB,4BAA4B,CAChD,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,yBAAyB,EAAE,CAAC,CA+BtC;AAKD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,MAAM,CAAA;CACtC,GAAG,IAAI,CAaP"}
1
+ {"version":3,"file":"prebuildWithoutExpo.d.ts","sourceRoot":"","sources":["../../src/exports/prebuildWithoutExpo.ts"],"names":[],"mappings":"AAkBA,OAAO,EAKL,iBAAiB,EAEjB,KAAK,iBAAiB,EAEvB,MAAM,+BAA+B,CAAA;AAwEtC,YAAY,EAAE,iBAAiB,IAAI,iBAAiB,EAAE,MAAM,+BAA+B,CAAA;AAE3F,eAAO,MAAM,mBAAmB,0BAAoB,CAAA;AA6EpD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAwChE;AAyJD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,WAAW,CAAC,WAAW,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,MAAM,CAyDrI;AAqnCD,MAAM,WAAW,oBAAoB;IACnC,gBAAgB,EAAE,MAAM,CAAA;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AA6sBD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,EAAE,KAAK,GAAG,SAAS,CAAA;IAC3B,GAAG,EAAE,iBAAiB,CAAA;IACtB,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB,GAAG,oBAAoB,CA0xBvB;AAED,eAAO,MAAM,mBAAmB,SACxB,MAAM,YACF,KAAK,GAAG,SAAS,OACtB,iBAAiB,WACd,MAAM,kBAwEf,CAAA;AA6DD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,QA6EvD;AAID,wBAAsB,qBAAqB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,iBAsDzF;AAyFD,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAClD,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,iBAAiB,CAAA;IACtB,SAAS,EAAE,SAAS,yBAAyB,EAAE,CAAA;CAChD,GAAG,IAAI,CAmCP;AAID,wBAAsB,4BAA4B,CAChD,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,yBAAyB,EAAE,CAAC,CA+BtC;AAKD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,MAAM,CAAA;CACtC,GAAG,IAAI,CAaP"}