@thelacanians/vue-native-cli 0.4.4 → 0.4.5

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.
Files changed (35) hide show
  1. package/dist/cli.js +71 -18
  2. package/native/android/VueNativeCore/build.gradle.kts +16 -3
  3. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VListFactory.kt +1 -1
  4. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VModalFactory.kt +1 -1
  5. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Helpers/TouchableView.kt +1 -1
  6. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/AudioModule.kt +3 -3
  7. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/BluetoothModule.kt +1 -1
  8. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/IAPModule.kt +8 -8
  9. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/SocialAuthModule.kt +6 -3
  10. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Styling/StyleEngine.kt +10 -10
  11. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/CertificatePinning.swift +6 -2
  12. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/ErrorOverlayView.swift +1 -4
  13. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/NativeBridge.swift +15 -0
  14. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VActionSheetFactory.swift +1 -2
  15. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VAlertDialogFactory.swift +1 -2
  16. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VListFactory.swift +1 -1
  17. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VModalFactory.swift +1 -4
  18. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSectionListFactory.swift +1 -1
  19. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VVideoFactory.swift +15 -11
  20. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VWebViewFactory.swift +5 -3
  21. package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/Extensions.swift +31 -0
  22. package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/TouchableView.swift +1 -1
  23. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/AnimationModule.swift +0 -8
  24. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/AppStateModule.swift +20 -26
  25. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/AudioModule.swift +13 -8
  26. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/BackgroundTaskModule.swift +1 -1
  27. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/BluetoothModule.swift +0 -8
  28. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/CalendarModule.swift +0 -7
  29. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/CameraModule.swift +14 -17
  30. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/IAPModule.swift +2 -2
  31. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/PerformanceModule.swift +5 -2
  32. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/ShareModule.swift +1 -2
  33. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/SocialAuthModule.swift +24 -8
  34. package/native/ios/VueNativeCore/Sources/VueNativeCore/Styling/StyleEngine.swift +3 -3
  35. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import { join, dirname } from "path";
10
10
  import { fileURLToPath } from "url";
11
11
  import { existsSync } from "fs";
12
12
  import pc from "picocolors";
13
- var VERSION = "0.4.4";
13
+ var VERSION = "0.4.5";
14
14
  var createCommand = new Command("create").description("Create a new Vue Native project").argument("<name>", "project name").option("-t, --template <template>", "project template (blank, tabs, drawer)", "blank").action(async (name, options) => {
15
15
  const template = options.template;
16
16
  if (!["blank", "tabs", "drawer"].includes(template)) {
@@ -226,12 +226,20 @@ class AppViewController: VueNativeViewController {
226
226
  const androidPkgPath = androidPkg.replace(/\./g, "/");
227
227
  const androidSrcDir = join(androidAppDir, "src", "main");
228
228
  const androidKotlinDir = join(androidSrcDir, "kotlin", androidPkgPath);
229
+ const androidResValuesDir = join(androidSrcDir, "res", "values");
230
+ const androidResXmlDir = join(androidSrcDir, "res", "xml");
231
+ const androidDebugResXmlDir = join(androidAppDir, "src", "debug", "res", "xml");
232
+ const androidGradleWrapperDir = join(androidDir, "gradle", "wrapper");
229
233
  await mkdir(androidKotlinDir, { recursive: true });
234
+ await mkdir(androidResValuesDir, { recursive: true });
235
+ await mkdir(androidResXmlDir, { recursive: true });
236
+ await mkdir(androidDebugResXmlDir, { recursive: true });
237
+ await mkdir(androidGradleWrapperDir, { recursive: true });
230
238
  await writeFile(join(androidDir, "build.gradle.kts"), `// Top-level build file
231
239
  plugins {
232
- id("com.android.application") version "8.2.2" apply false
233
- id("com.android.library") version "8.2.2" apply false
234
- id("org.jetbrains.kotlin.android") version "1.9.22" apply false
240
+ id("com.android.application") version "8.7.3" apply false
241
+ id("com.android.library") version "8.7.3" apply false
242
+ id("org.jetbrains.kotlin.android") version "2.0.21" apply false
235
243
  }
236
244
  `);
237
245
  await writeFile(join(androidDir, "settings.gradle.kts"), `pluginManagement {
@@ -250,8 +258,8 @@ dependencyResolutionManagement {
250
258
  maven {
251
259
  url = uri("https://maven.pkg.github.com/abdul-hamid-achik/vue-native")
252
260
  credentials {
253
- username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
254
- password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
261
+ username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR") ?: ""
262
+ password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN") ?: ""
255
263
  }
256
264
  }
257
265
  }
@@ -267,19 +275,19 @@ include(":app")
267
275
 
268
276
  android {
269
277
  namespace = "${androidPkg}"
270
- compileSdk = 34
278
+ compileSdk = 35
271
279
 
272
280
  defaultConfig {
273
281
  applicationId = "${androidPkg}"
274
- minSdk = 21
275
- targetSdk = 34
282
+ minSdk = 24
283
+ targetSdk = 35
276
284
  versionCode = 1
277
285
  versionName = "1.0"
278
286
  }
279
287
 
280
288
  buildTypes {
281
289
  release {
282
- isMinifyEnabled = false
290
+ isMinifyEnabled = true
283
291
  proguardFiles(
284
292
  getDefaultProguardFile("proguard-android-optimize.txt"),
285
293
  "proguard-rules.pro"
@@ -287,6 +295,10 @@ android {
287
295
  }
288
296
  }
289
297
 
298
+ buildFeatures {
299
+ buildConfig = true
300
+ }
301
+
290
302
  compileOptions {
291
303
  sourceCompatibility = JavaVersion.VERSION_17
292
304
  targetCompatibility = JavaVersion.VERSION_17
@@ -299,7 +311,16 @@ android {
299
311
 
300
312
  dependencies {
301
313
  implementation("com.vuenative:core:${VERSION}")
314
+ implementation("androidx.appcompat:appcompat:1.7.0")
315
+ implementation("com.google.android.material:material:1.12.0")
316
+ implementation("androidx.core:core-ktx:1.15.0")
302
317
  }
318
+ `);
319
+ await writeFile(join(androidAppDir, "proguard-rules.pro"), `# Vue Native
320
+ -keep class com.vuenative.** { *; }
321
+
322
+ # J2V8
323
+ -keep class com.eclipsesource.v8.** { *; }
303
324
  `);
304
325
  await writeFile(join(androidSrcDir, "AndroidManifest.xml"), `<?xml version="1.0" encoding="utf-8"?>
305
326
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
@@ -307,13 +328,15 @@ dependencies {
307
328
 
308
329
  <application
309
330
  android:allowBackup="true"
310
- android:label="${name}"
331
+ android:label="@string/app_name"
311
332
  android:supportsRtl="true"
312
- android:theme="@style/Theme.AppCompat.Light.NoActionBar"
333
+ android:theme="@style/Theme.VueNative"
313
334
  android:networkSecurityConfig="@xml/network_security_config">
314
335
  <activity
315
336
  android:name=".MainActivity"
316
- android:exported="true">
337
+ android:exported="true"
338
+ android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|keyboard|locale|layoutDirection|fontScale|uiMode|density"
339
+ android:windowSoftInputMode="adjustResize">
317
340
  <intent-filter>
318
341
  <action android:name="android.intent.action.MAIN" />
319
342
  <category android:name="android.intent.category.LAUNCHER" />
@@ -322,9 +345,30 @@ dependencies {
322
345
  </application>
323
346
  </manifest>
324
347
  `);
325
- const androidResXmlDir = join(androidSrcDir, "res", "xml");
326
- await mkdir(androidResXmlDir, { recursive: true });
348
+ await writeFile(join(androidResValuesDir, "strings.xml"), `<?xml version="1.0" encoding="utf-8"?>
349
+ <resources>
350
+ <string name="app_name">${name}</string>
351
+ </resources>
352
+ `);
353
+ await writeFile(join(androidResValuesDir, "themes.xml"), `<?xml version="1.0" encoding="utf-8"?>
354
+ <resources>
355
+ <style name="Theme.VueNative" parent="Theme.MaterialComponents.Light.NoActionBar">
356
+ <item name="colorPrimary">#4F46E5</item>
357
+ <item name="colorPrimaryVariant">#3730A3</item>
358
+ <item name="colorOnPrimary">#FFFFFF</item>
359
+ <item name="colorSecondary">#10B981</item>
360
+ <item name="colorSecondaryVariant">#059669</item>
361
+ <item name="colorOnSecondary">#FFFFFF</item>
362
+ <item name="android:statusBarColor">@android:color/transparent</item>
363
+ </style>
364
+ </resources>
365
+ `);
327
366
  await writeFile(join(androidResXmlDir, "network_security_config.xml"), `<?xml version="1.0" encoding="utf-8"?>
367
+ <network-security-config>
368
+ <base-config cleartextTrafficPermitted="false" />
369
+ </network-security-config>
370
+ `);
371
+ await writeFile(join(androidDebugResXmlDir, "network_security_config.xml"), `<?xml version="1.0" encoding="utf-8"?>
328
372
  <network-security-config>
329
373
  <domain-config cleartextTrafficPermitted="true">
330
374
  <domain includeSubdomains="true">localhost</domain>
@@ -343,7 +387,7 @@ class MainActivity : VueNativeActivity() {
343
387
  }
344
388
 
345
389
  override fun getDevServerUrl(): String? {
346
- return "ws://10.0.2.2:8174"
390
+ return if (BuildConfig.DEBUG) "ws://10.0.2.2:8174" else null
347
391
  }
348
392
  }
349
393
  `);
@@ -352,6 +396,13 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
352
396
  android.useAndroidX=true
353
397
  kotlin.code.style=official
354
398
  android.nonTransitiveRClass=true
399
+ `);
400
+ await writeFile(join(androidGradleWrapperDir, "gradle-wrapper.properties"), `distributionBase=GRADLE_USER_HOME
401
+ distributionPath=wrapper/dists
402
+ distributionUrl=https\\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
403
+ networkTimeout=10000
404
+ zipStoreBase=GRADLE_USER_HOME
405
+ zipStorePath=wrapper/dists
355
406
  `);
356
407
  await writeFile(join(dir, "vue-native.config.ts"), `import { defineConfig } from '@thelacanians/vue-native-cli'
357
408
 
@@ -363,8 +414,8 @@ export default defineConfig({
363
414
  deploymentTarget: '16.0',
364
415
  },
365
416
  android: {
366
- minSdk: 21,
367
- targetSdk: 34,
417
+ minSdk: 24,
418
+ targetSdk: 35,
368
419
  },
369
420
  })
370
421
  `);
@@ -413,6 +464,8 @@ local.properties
413
464
  console.log(pc.white(" To run on iOS:"));
414
465
  console.log(pc.white(" vue-native run ios\n"));
415
466
  console.log(pc.white(" To run on Android:"));
467
+ console.log(pc.dim(" Open android/ in Android Studio, or run:"));
468
+ console.log(pc.dim(" cd android && gradle wrapper && cd .."));
416
469
  console.log(pc.white(" vue-native run android\n"));
417
470
  } catch (err) {
418
471
  console.error(pc.red(`Error creating project: ${err.message}`));
@@ -6,11 +6,11 @@ plugins {
6
6
 
7
7
  android {
8
8
  namespace = "com.vuenative.core"
9
- compileSdk = 34
9
+ compileSdk = 35
10
10
 
11
11
  defaultConfig {
12
12
  minSdk = 21
13
- targetSdk = 34
13
+ targetSdk = 35
14
14
 
15
15
  testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
16
16
  consumerProguardFiles("consumer-rules.pro")
@@ -66,6 +66,12 @@ dependencies {
66
66
  // Kotlin Coroutines
67
67
  implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
68
68
 
69
+ // Lifecycle Process (for ProcessLifecycleOwner)
70
+ implementation("androidx.lifecycle:lifecycle-process:2.7.0")
71
+
72
+ // WorkManager (for BackgroundTaskModule)
73
+ implementation("androidx.work:work-runtime-ktx:2.8.1")
74
+
69
75
  // Location (for GeolocationModule)
70
76
  implementation("com.google.android.gms:play-services-location:21.1.0")
71
77
 
@@ -74,6 +80,13 @@ dependencies {
74
80
 
75
81
  // Secure Storage (for SecureStorageModule)
76
82
  implementation("androidx.security:security-crypto:1.1.0-alpha06")
83
+
84
+ // Google Play Billing (for IAPModule)
85
+ implementation("com.android.billingclient:billing:7.0.0")
86
+
87
+ // Credential Manager + Google Identity (for SocialAuthModule)
88
+ implementation("androidx.credentials:credentials:1.2.2")
89
+ implementation("com.google.android.libraries.identity.googleid:googleid:1.1.1")
77
90
  }
78
91
 
79
92
  afterEvaluate {
@@ -82,7 +95,7 @@ afterEvaluate {
82
95
  create<MavenPublication>("release") {
83
96
  groupId = "com.vuenative"
84
97
  artifactId = "core"
85
- version = "0.4.4"
98
+ version = "0.4.5"
86
99
  from(components["release"])
87
100
  }
88
101
  }
@@ -157,7 +157,7 @@ class VListFactory : NativeComponentFactory {
157
157
  /**
158
158
  * Called when the parent view is being destroyed. Cleans up the RecyclerView.
159
159
  */
160
- fun destroyView(view: View) {
160
+ override fun destroyView(view: View) {
161
161
  val rv = view as? RecyclerView ?: return
162
162
  cleanupRecyclerView(rv)
163
163
  }
@@ -94,7 +94,7 @@ class VModalFactory : NativeComponentFactory {
94
94
  * Dismisses the dialog and clears all map entries that reference the view,
95
95
  * preventing memory leaks.
96
96
  */
97
- fun destroyView(view: View) {
97
+ override fun destroyView(view: View) {
98
98
  dialogs[view]?.let { dialog ->
99
99
  if (dialog.isShowing) dialog.dismiss()
100
100
  }
@@ -14,7 +14,7 @@ import com.google.android.flexbox.JustifyContent
14
14
  * with configurable active opacity and support for press and long press events.
15
15
  * Mirrors the Swift TouchableView (UIView subclass) for Android.
16
16
  */
17
- class TouchableView(context: Context) : FlexboxLayout(context) {
17
+ open class TouchableView(context: Context) : FlexboxLayout(context) {
18
18
 
19
19
  /** The opacity to apply when the user is pressing the view. */
20
20
  var activeOpacity: Float = 1.0f
@@ -108,14 +108,14 @@ class AudioModule : NativeModule {
108
108
  mp.setOnCompletionListener {
109
109
  isPlaying = false
110
110
  stopProgressReporting()
111
- bridge?.sendGlobalEvent("audio:complete", emptyMap())
111
+ bridge?.dispatchGlobalEvent("audio:complete", emptyMap())
112
112
  }
113
113
 
114
114
  mp.setOnErrorListener { _, what, extra ->
115
115
  isPlaying = false
116
116
  stopProgressReporting()
117
117
  val msg = "MediaPlayer error: what=$what extra=$extra"
118
- bridge?.sendGlobalEvent("audio:error", mapOf("message" to msg))
118
+ bridge?.dispatchGlobalEvent("audio:error", mapOf("message" to msg))
119
119
  true
120
120
  }
121
121
 
@@ -179,7 +179,7 @@ class AudioModule : NativeModule {
179
179
  override fun run() {
180
180
  val mp = player ?: return
181
181
  if (isPlaying) {
182
- bridge?.sendGlobalEvent("audio:progress", mapOf(
182
+ bridge?.dispatchGlobalEvent("audio:progress", mapOf(
183
183
  "currentTime" to mp.currentPosition.toDouble() / 1000.0,
184
184
  "duration" to mp.duration.toDouble() / 1000.0
185
185
  ))
@@ -90,7 +90,7 @@ class BluetoothModule : NativeModule {
90
90
  "getState" -> {
91
91
  val state = when {
92
92
  bluetoothAdapter == null -> "unsupported"
93
- !bluetoothAdapter!!.isEnabled -> "poweredOff"
93
+ bluetoothAdapter?.isEnabled != true -> "poweredOff"
94
94
  else -> "poweredOn"
95
95
  }
96
96
  callback(state, null)
@@ -148,9 +148,9 @@ class IAPModule : NativeModule, PurchasesUpdatedListener {
148
148
  if (offerToken != null) {
149
149
  productBuilder.setOfferToken(offerToken)
150
150
  }
151
- flowParamsBuilder.setProductList(listOf(productBuilder.build()))
151
+ flowParamsBuilder.setProductDetailsParamsList(listOf(productBuilder.build()))
152
152
 
153
- val activity = bridge.activity ?: run {
153
+ val activity = (context as? android.app.Activity) ?: run {
154
154
  callback(null, "purchase: no activity available")
155
155
  pendingPurchaseCallback = null
156
156
  return
@@ -178,14 +178,14 @@ class IAPModule : NativeModule, PurchasesUpdatedListener {
178
178
  acknowledgePurchase(purchase)
179
179
  val info = mapOf(
180
180
  "productId" to (purchase.products.firstOrNull() ?: ""),
181
- "transactionId" to purchase.orderId,
181
+ "transactionId" to (purchase.orderId ?: ""),
182
182
  "purchaseDate" to purchase.purchaseTime.toString(),
183
183
  )
184
184
  callback?.invoke(info, null)
185
- bridge?.emitGlobalEvent("iap:transactionUpdate", mapOf(
185
+ bridge?.dispatchGlobalEvent("iap:transactionUpdate", mapOf(
186
186
  "productId" to (purchase.products.firstOrNull() ?: ""),
187
187
  "state" to "purchased",
188
- "transactionId" to purchase.orderId,
188
+ "transactionId" to (purchase.orderId ?: ""),
189
189
  ))
190
190
  } else {
191
191
  callback?.invoke(null, "purchase: no purchase returned")
@@ -193,7 +193,7 @@ class IAPModule : NativeModule, PurchasesUpdatedListener {
193
193
  }
194
194
  BillingClient.BillingResponseCode.USER_CANCELED -> {
195
195
  callback?.invoke(null, "purchase: user cancelled")
196
- bridge?.emitGlobalEvent("iap:transactionUpdate", mapOf(
196
+ bridge?.dispatchGlobalEvent("iap:transactionUpdate", mapOf(
197
197
  "productId" to "",
198
198
  "state" to "failed",
199
199
  "error" to "user cancelled",
@@ -202,7 +202,7 @@ class IAPModule : NativeModule, PurchasesUpdatedListener {
202
202
  else -> {
203
203
  val error = "purchase failed: ${result.debugMessage}"
204
204
  callback?.invoke(null, error)
205
- bridge?.emitGlobalEvent("iap:transactionUpdate", mapOf(
205
+ bridge?.dispatchGlobalEvent("iap:transactionUpdate", mapOf(
206
206
  "productId" to "",
207
207
  "state" to "failed",
208
208
  "error" to error,
@@ -291,7 +291,7 @@ class IAPModule : NativeModule, PurchasesUpdatedListener {
291
291
 
292
292
  private fun purchaseToMap(purchase: Purchase): Map<String, Any?> = mapOf(
293
293
  "productId" to (purchase.products.firstOrNull() ?: ""),
294
- "transactionId" to purchase.orderId,
294
+ "transactionId" to (purchase.orderId ?: ""),
295
295
  "purchaseDate" to purchase.purchaseTime.toString(),
296
296
  )
297
297
 
@@ -10,6 +10,9 @@ import androidx.credentials.*
10
10
  import androidx.credentials.exceptions.GetCredentialCancellationException
11
11
  import com.google.android.libraries.identity.googleid.GetGoogleIdOption
12
12
  import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
13
+ import kotlinx.coroutines.Dispatchers
14
+ import kotlinx.coroutines.GlobalScope
15
+ import kotlinx.coroutines.launch
13
16
 
14
17
  /**
15
18
  * Native module for social authentication (Google Sign In on Android).
@@ -62,10 +65,10 @@ class SocialAuthModule : NativeModule {
62
65
 
63
66
  private fun handleGoogleSignIn(clientId: String, callback: (Any?, String?) -> Unit) {
64
67
  val ctx = context ?: run { callback(null, "SocialAuth: no context"); return }
65
- val activity = bridge?.activity
68
+ val activity = ctx as? Activity
66
69
 
67
70
  if (activity == null) {
68
- callback(null, "signInWithGoogle: no activity available")
71
+ callback(null, "signInWithGoogle: no activity available (context is not an Activity)")
69
72
  return
70
73
  }
71
74
 
@@ -81,7 +84,7 @@ class SocialAuthModule : NativeModule {
81
84
  .build()
82
85
 
83
86
  mainHandler.post {
84
- kotlinx.coroutines.GlobalScope.launch(kotlinx.coroutines.Dispatchers.Main) {
87
+ GlobalScope.launch(Dispatchers.Main) {
85
88
  try {
86
89
  val result = credentialManager.getCredential(activity, request)
87
90
  val credential = result.credential
@@ -147,7 +147,7 @@ object StyleEngine {
147
147
  }
148
148
  "minWidth" -> updateFlexProps(view) { fp -> fp.copy(minWidth = dpToPx(ctx, toFloat(value, 0f)).toInt()) }
149
149
  "minHeight" -> updateFlexProps(view) { fp -> fp.copy(minHeight = dpToPx(ctx, toFloat(value, 0f)).toInt()) }
150
- "maxWidth" -> view.post { view.maxWidth = dpToPx(ctx, toFloat(value, 0f)).toInt() }
150
+ "maxWidth" -> updateFlexProps(view) { fp -> fp.copy(maxWidth = dpToPx(ctx, toFloat(value, 0f)).toInt()) }
151
151
 
152
152
  // --- Flex props (stored in FlexProps, applied when inserted) ---
153
153
  "flex" -> {
@@ -192,17 +192,14 @@ object StyleEngine {
192
192
  "gap" -> {
193
193
  val px = dpToPx(ctx, toFloat(value, 0f)).toInt()
194
194
  view.setTag(TAG_GAP, px)
195
- (view as? FlexboxLayout)?.gap = px
196
195
  }
197
196
  "rowGap" -> {
198
197
  val px = dpToPx(ctx, toFloat(value, 0f)).toInt()
199
198
  view.setTag(TAG_GAP, px)
200
- (view as? FlexboxLayout)?.rowGap = px
201
199
  }
202
200
  "columnGap" -> {
203
201
  val px = dpToPx(ctx, toFloat(value, 0f)).toInt()
204
202
  view.setTag(TAG_GAP, px)
205
- (view as? FlexboxLayout)?.columnGap = px
206
203
  }
207
204
 
208
205
  // --- Position (absolute) ---
@@ -424,8 +421,8 @@ object StyleEngine {
424
421
  tv.paintFlags = when (value) {
425
422
  "underline" -> tv.paintFlags or android.graphics.Paint.UNDERLINE_TEXT_FLAG
426
423
  "line-through" -> tv.paintFlags or android.graphics.Paint.STRIKE_THRU_TEXT_FLAG
427
- "none" -> tv.paintFlags and android.graphics.Paint.UNDERLINE_TEXT_FLAG.inv()
428
- and android.graphics.Paint.STRIKE_THRU_TEXT_FLAG.inv()
424
+ "none" -> (tv.paintFlags and android.graphics.Paint.UNDERLINE_TEXT_FLAG.inv()
425
+ and android.graphics.Paint.STRIKE_THRU_TEXT_FLAG.inv())
429
426
  else -> tv.paintFlags
430
427
  }
431
428
  }
@@ -467,6 +464,8 @@ object StyleEngine {
467
464
  val order: Int = 1,
468
465
  val minWidth: Int = 0,
469
466
  val minHeight: Int = 0,
467
+ val maxWidth: Int = Int.MAX_VALUE,
468
+ val maxHeight: Int = Int.MAX_VALUE,
470
469
  )
471
470
 
472
471
  fun getFlexProps(view: View): FlexProps =
@@ -501,9 +500,10 @@ object StyleEngine {
501
500
  order = fp.order
502
501
  minWidth = fp.minWidth
503
502
  minHeight = fp.minHeight
504
- // Apply percentage dimensions when set (FlexboxLayout 3.x widthPercent/heightPercent)
505
- if (fp.widthPercent >= 0f) widthPercent = fp.widthPercent
506
- if (fp.heightPercent >= 0f) heightPercent = fp.heightPercent
503
+ maxWidth = fp.maxWidth
504
+ maxHeight = fp.maxHeight
505
+ // Percentage dimensions stored as tags for custom layout logic
506
+ // (FlexboxLayout.LayoutParams does not have widthPercent/heightPercent)
507
507
  }
508
508
  }
509
509
 
@@ -602,7 +602,7 @@ object StyleEngine {
602
602
  private fun parseFlexWrap(value: Any?) = when (value) {
603
603
  "wrap" -> FlexWrap.WRAP
604
604
  "wrap-reverse" -> FlexWrap.WRAP_REVERSE
605
- else -> FlexWrap.NO_WRAP
605
+ else -> FlexWrap.NOWRAP
606
606
  }
607
607
 
608
608
  private fun parseAlignItems(value: Any?) = when (value) {
@@ -94,9 +94,13 @@ public final class CertificatePinning: NSObject, URLSessionDelegate {
94
94
 
95
95
  // Check each certificate in the chain against our pins
96
96
  let certCount = SecTrustGetCertificateCount(serverTrust)
97
+ guard let certChain = SecTrustCopyCertificateChain(serverTrust) as? [SecCertificate] else {
98
+ completionHandler(.cancelAuthenticationChallenge, nil)
99
+ return
100
+ }
97
101
  for i in 0..<certCount {
98
- guard let cert = SecTrustCopyCertificateChain(serverTrust)?[i] else { continue }
99
- guard let certificate = cert as? SecCertificate else { continue }
102
+ guard i < certChain.count else { continue }
103
+ let certificate = certChain[i]
100
104
  let spkiHash = sha256OfSPKI(for: certificate)
101
105
  if expectedPins.contains(spkiHash) {
102
106
  completionHandler(.useCredential, URLCredential(trust: serverTrust))
@@ -76,10 +76,7 @@ final class ErrorOverlayView: UIView {
76
76
 
77
77
  static func show(error: String) {
78
78
  DispatchQueue.main.async {
79
- guard let window = UIApplication.shared.connectedScenes
80
- .compactMap({ $0 as? UIWindowScene })
81
- .flatMap({ $0.windows })
82
- .first(where: { $0.isKeyWindow }) else { return }
79
+ guard let window = UIApplication.shared.vn_keyWindow else { return }
83
80
 
84
81
  // Remove any existing overlay
85
82
  window.subviews.compactMap { $0 as? ErrorOverlayView }.forEach { $0.removeFromSuperview() }
@@ -879,8 +879,23 @@ public final class NativeBridge {
879
879
  private final class TraitObserverView: UIView {
880
880
  var onChange: ((Bool) -> Void)?
881
881
 
882
+ override init(frame: CGRect) {
883
+ super.init(frame: frame)
884
+ if #available(iOS 17.0, *) {
885
+ registerForTraitChanges([UITraitUserInterfaceStyle.self]) { [weak self] (view: TraitObserverView, _: UITraitCollection) in
886
+ self?.onChange?(view.traitCollection.userInterfaceStyle == .dark)
887
+ }
888
+ }
889
+ }
890
+
891
+ required init?(coder: NSCoder) {
892
+ super.init(coder: coder)
893
+ }
894
+
895
+ // Fallback for iOS 16
882
896
  override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
883
897
  super.traitCollectionDidChange(previousTraitCollection)
898
+ if #available(iOS 17.0, *) { return }
884
899
  if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
885
900
  onChange?(traitCollection.userInterfaceStyle == .dark)
886
901
  }
@@ -58,8 +58,7 @@ final class VActionSheetFactory: NativeComponentFactory {
58
58
  })
59
59
  }
60
60
 
61
- guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
62
- let rootVC = scene.windows.first?.rootViewController else { return }
61
+ guard let rootVC = UIApplication.shared.vn_keyWindow?.rootViewController else { return }
63
62
  var top = rootVC
64
63
  while let p = top.presentedViewController { top = p }
65
64
 
@@ -140,8 +140,7 @@ final class VAlertDialogFactory: NativeComponentFactory {
140
140
  }
141
141
 
142
142
  // Find the topmost presented view controller to present from
143
- guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
144
- let rootVC = scene.windows.first?.rootViewController else { return }
143
+ guard let rootVC = UIApplication.shared.vn_keyWindow?.rootViewController else { return }
145
144
  var topVC = rootVC
146
145
  while let presented = topVC.presentedViewController { topVC = presented }
147
146
  topVC.present(alert, animated: true)
@@ -215,7 +215,7 @@ private final class VListInternalDelegate: NSObject,
215
215
 
216
216
  // MARK: - VListCell
217
217
 
218
- private final class VListCell: UITableViewCell {
218
+ final class VListCell: UITableViewCell {
219
219
 
220
220
  private var currentView: UIView?
221
221
 
@@ -39,10 +39,7 @@ final class VModalFactory: NativeComponentFactory {
39
39
  }
40
40
 
41
41
  private func showOverlay(for placeholder: UIView) {
42
- guard let window = UIApplication.shared.connectedScenes
43
- .compactMap({ $0 as? UIWindowScene })
44
- .flatMap({ $0.windows })
45
- .first(where: { $0.isKeyWindow }) else { return }
42
+ guard let window = UIApplication.shared.vn_keyWindow else { return }
46
43
 
47
44
  // Get or create overlay
48
45
  let overlay: UIView
@@ -221,7 +221,7 @@ private final class VSectionListInternalDelegate: NSObject,
221
221
  return container?.estimatedItemHeight ?? 44
222
222
  }
223
223
  let h = container.sections[indexPath.section].itemViews[indexPath.row].frame.size.height
224
- return h > 1 ? h : (container?.estimatedItemHeight ?? 44)
224
+ return h > 1 ? h : container.estimatedItemHeight
225
225
  }
226
226
 
227
227
  func scrollViewDidScroll(_ scrollView: UIScrollView) {
@@ -5,6 +5,12 @@ import AVKit
5
5
  import FlexLayout
6
6
  import ObjectiveC
7
7
 
8
+ // File-level keys used in non-isolated contexts (e.g. deinit).
9
+ // Must live outside the @MainActor-isolated class so they can be
10
+ // passed as `inout` without actor-isolation violations.
11
+ private nonisolated(unsafe) var _timeObserverKey: UInt8 = 7
12
+ private nonisolated(unsafe) var _endObserverKey: UInt8 = 9
13
+
8
14
  /// Factory for VVideo — the video playback component.
9
15
  /// Uses AVPlayer + AVPlayerLayer for inline video playback.
10
16
  final class VVideoFactory: NativeComponentFactory {
@@ -18,9 +24,7 @@ final class VVideoFactory: NativeComponentFactory {
18
24
  private static var onErrorKey: UInt8 = 4
19
25
  private static var onProgressKey: UInt8 = 5
20
26
  private static var playerKey: UInt8 = 6
21
- private static var timeObserverKey: UInt8 = 7
22
27
  private static var statusObserverKey: UInt8 = 8
23
- private static var endObserverKey: UInt8 = 9
24
28
 
25
29
  // MARK: - NativeComponentFactory
26
30
 
@@ -169,11 +173,11 @@ final class VVideoFactory: NativeComponentFactory {
169
173
  payload: ["currentTime": currentTime, "duration": dur])
170
174
  }
171
175
  }
172
- objc_setAssociatedObject(container, &VVideoFactory.timeObserverKey, timeObserver as AnyObject, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
176
+ objc_setAssociatedObject(container, &_timeObserverKey, timeObserver as AnyObject, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
173
177
 
174
178
  // End observer
175
179
  let endObserver = NotificationCenter.default.addObserver(
176
- forName: .AVPlayerItemDidPlayToEndOfTime,
180
+ forName: .AVPlayerItemDidPlayToEndTime,
177
181
  object: playerItem,
178
182
  queue: .main
179
183
  ) { [weak container] _ in
@@ -184,20 +188,20 @@ final class VVideoFactory: NativeComponentFactory {
184
188
  container.player?.play()
185
189
  }
186
190
  }
187
- objc_setAssociatedObject(container, &VVideoFactory.endObserverKey, endObserver as AnyObject, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
191
+ objc_setAssociatedObject(container, &_endObserverKey, endObserver as AnyObject, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
188
192
  }
189
193
 
190
194
  private func cleanupPlayer(for container: VideoContainerView) {
191
195
  // Remove time observer
192
- if let timeObserver = objc_getAssociatedObject(container, &VVideoFactory.timeObserverKey) {
196
+ if let timeObserver = objc_getAssociatedObject(container, &_timeObserverKey) {
193
197
  container.player?.removeTimeObserver(timeObserver)
194
- objc_setAssociatedObject(container, &VVideoFactory.timeObserverKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
198
+ objc_setAssociatedObject(container, &_timeObserverKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
195
199
  }
196
200
 
197
201
  // Remove end observer
198
- if let endObserver = objc_getAssociatedObject(container, &VVideoFactory.endObserverKey) {
202
+ if let endObserver = objc_getAssociatedObject(container, &_endObserverKey) {
199
203
  NotificationCenter.default.removeObserver(endObserver)
200
- objc_setAssociatedObject(container, &VVideoFactory.endObserverKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
204
+ objc_setAssociatedObject(container, &_endObserverKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
201
205
  }
202
206
 
203
207
  // Remove status observer
@@ -232,10 +236,10 @@ private class VideoContainerView: UIView {
232
236
 
233
237
  deinit {
234
238
  // Clean up player resources
235
- if let timeObserver = objc_getAssociatedObject(self, &VVideoFactory.timeObserverKey) {
239
+ if let timeObserver = objc_getAssociatedObject(self, &_timeObserverKey) {
236
240
  player?.removeTimeObserver(timeObserver)
237
241
  }
238
- if let endObserver = objc_getAssociatedObject(self, &VVideoFactory.endObserverKey) {
242
+ if let endObserver = objc_getAssociatedObject(self, &_endObserverKey) {
239
243
  NotificationCenter.default.removeObserver(endObserver)
240
244
  }
241
245
  player?.pause()
@@ -12,9 +12,11 @@ import FlexLayout
12
12
  final class VWebViewFactory: NativeComponentFactory {
13
13
 
14
14
  // fileprivate so the inner delegate/handler classes in this file can access them.
15
- fileprivate static var onLoadKey: UInt8 = 0
16
- fileprivate static var onErrorKey: UInt8 = 1
17
- fileprivate static var onMessageKey: UInt8 = 2
15
+ // nonisolated(unsafe) allows usage from non-isolated contexts (WKNavigationDelegate,
16
+ // WKScriptMessageHandler callbacks) without actor-isolation errors.
17
+ nonisolated(unsafe) fileprivate static var onLoadKey: UInt8 = 0
18
+ nonisolated(unsafe) fileprivate static var onErrorKey: UInt8 = 1
19
+ nonisolated(unsafe) fileprivate static var onMessageKey: UInt8 = 2
18
20
  fileprivate static var delegateKey: UInt8 = 3
19
21
  fileprivate static var msgHandlerKey: UInt8 = 4
20
22
 
@@ -0,0 +1,31 @@
1
+ #if canImport(UIKit)
2
+ import UIKit
3
+
4
+ // MARK: - Safe array subscript
5
+
6
+ extension Array {
7
+ subscript(safe index: Int) -> Element? {
8
+ indices.contains(index) ? self[index] : nil
9
+ }
10
+ }
11
+
12
+ // MARK: - Key window helper (avoids deprecated UIWindowScene.windows)
13
+
14
+ extension UIApplication {
15
+ /// Returns the key window from the first foreground-active window scene.
16
+ var vn_keyWindow: UIWindow? {
17
+ connectedScenes
18
+ .compactMap { $0 as? UIWindowScene }
19
+ .compactMap { $0.keyWindow }
20
+ .first
21
+ }
22
+
23
+ /// Returns the topmost presented view controller.
24
+ var vn_topViewController: UIViewController? {
25
+ guard let root = vn_keyWindow?.rootViewController else { return nil }
26
+ var top = root
27
+ while let presented = top.presentedViewController { top = presented }
28
+ return top
29
+ }
30
+ }
31
+ #endif
@@ -3,7 +3,7 @@ import UIKit
3
3
 
4
4
  /// Custom UIView subclass that provides button-like touch behavior
5
5
  /// with configurable active opacity and support for press and long press events.
6
- final class TouchableView: UIView {
6
+ class TouchableView: UIView {
7
7
 
8
8
  // MARK: - Public properties
9
9
 
@@ -280,12 +280,4 @@ final class AnimationModule: NativeModule {
280
280
  }
281
281
  }
282
282
 
283
- // MARK: - Array safe subscript
284
-
285
- private extension Array {
286
- subscript(safe index: Int) -> Element? {
287
- guard index >= 0 && index < count else { return nil }
288
- return self[index]
289
- }
290
- }
291
283
  #endif
@@ -1,43 +1,37 @@
1
1
  #if canImport(UIKit)
2
2
  import UIKit
3
3
 
4
- final class AppStateModule: NativeModule {
4
+ final class AppStateModule: NSObject, NativeModule {
5
5
  var moduleName: String { "AppState" }
6
6
  private weak var bridge: NativeBridge?
7
+ private var observers: [NSObjectProtocol] = []
7
8
 
8
9
  init(bridge: NativeBridge) {
9
10
  self.bridge = bridge
11
+ super.init()
10
12
  setupObservers()
11
13
  }
12
14
 
13
15
  private func setupObservers() {
14
- NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive),
15
- name: UIApplication.didBecomeActiveNotification, object: nil)
16
- NotificationCenter.default.addObserver(self, selector: #selector(willResignActive),
17
- name: UIApplication.willResignActiveNotification, object: nil)
18
- NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground),
19
- name: UIApplication.didEnterBackgroundNotification, object: nil)
16
+ observers.append(NotificationCenter.default.addObserver(
17
+ forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main
18
+ ) { [weak self] _ in
19
+ self?.bridge?.dispatchGlobalEvent("appState:change", payload: ["state": "active"])
20
+ })
21
+ observers.append(NotificationCenter.default.addObserver(
22
+ forName: UIApplication.willResignActiveNotification, object: nil, queue: .main
23
+ ) { [weak self] _ in
24
+ self?.bridge?.dispatchGlobalEvent("appState:change", payload: ["state": "inactive"])
25
+ })
26
+ observers.append(NotificationCenter.default.addObserver(
27
+ forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main
28
+ ) { [weak self] _ in
29
+ self?.bridge?.dispatchGlobalEvent("appState:change", payload: ["state": "background"])
30
+ })
20
31
  }
21
32
 
22
- @objc private func didBecomeActive() {
23
- let bridge = bridge
24
- DispatchQueue.main.async {
25
- bridge?.dispatchGlobalEvent("appState:change", payload: ["state": "active"])
26
- }
27
- }
28
-
29
- @objc private func willResignActive() {
30
- let bridge = bridge
31
- DispatchQueue.main.async {
32
- bridge?.dispatchGlobalEvent("appState:change", payload: ["state": "inactive"])
33
- }
34
- }
35
-
36
- @objc private func didEnterBackground() {
37
- let bridge = bridge
38
- DispatchQueue.main.async {
39
- bridge?.dispatchGlobalEvent("appState:change", payload: ["state": "background"])
40
- }
33
+ deinit {
34
+ observers.forEach { NotificationCenter.default.removeObserver($0) }
41
35
  }
42
36
 
43
37
  func invoke(method: String, args: [Any], callback: @escaping (Any?, String?) -> Void) {
@@ -17,11 +17,11 @@ import AVFoundation
17
17
  /// - resumeRecording() -- resume recording
18
18
  /// - getStatus() -- returns current playback status
19
19
  ///
20
- /// Events (via bridge.sendGlobalEvent):
20
+ /// Events (via bridge.dispatchGlobalEvent):
21
21
  /// - audio:progress { currentTime, duration }
22
22
  /// - audio:complete {}
23
23
  /// - audio:error { message }
24
- final class AudioModule: NativeModule {
24
+ final class AudioModule: NSObject, NativeModule {
25
25
  let moduleName = "Audio"
26
26
 
27
27
  private var player: AVAudioPlayer?
@@ -116,7 +116,7 @@ final class AudioModule: NativeModule {
116
116
  guard let self = self else { return }
117
117
  if let error = error {
118
118
  callback(nil, "Failed to download audio: \(error.localizedDescription)")
119
- self.bridge?.sendGlobalEvent("audio:error", payload: ["message": error.localizedDescription])
119
+ self.bridge?.dispatchGlobalEvent("audio:error", payload: ["message": error.localizedDescription])
120
120
  return
121
121
  }
122
122
  guard let data = data else {
@@ -153,7 +153,9 @@ final class AudioModule: NativeModule {
153
153
  guard let self = self else { return }
154
154
  self.isPlaying = false
155
155
  self.stopProgressReporting()
156
- self.bridge?.sendGlobalEvent("audio:complete", payload: [:])
156
+ DispatchQueue.main.async { [weak self] in
157
+ self?.bridge?.dispatchGlobalEvent("audio:complete", payload: [:])
158
+ }
157
159
  }
158
160
  player.delegate = delegate
159
161
  self.playerDelegate = delegate
@@ -233,10 +235,13 @@ final class AudioModule: NativeModule {
233
235
 
234
236
  @objc private func reportProgress() {
235
237
  guard let player = player, isPlaying else { return }
236
- bridge?.sendGlobalEvent("audio:progress", payload: [
237
- "currentTime": player.currentTime,
238
- "duration": player.duration,
239
- ])
238
+ DispatchQueue.main.async { [weak self] in
239
+ guard let self = self, let player = self.player else { return }
240
+ self.bridge?.dispatchGlobalEvent("audio:progress", payload: [
241
+ "currentTime": player.currentTime,
242
+ "duration": player.duration,
243
+ ])
244
+ }
240
245
  }
241
246
 
242
247
  // MARK: - Recording
@@ -44,7 +44,7 @@ final class BackgroundTaskModule: NativeModule {
44
44
  callback(nil, "cancelTask: missing taskId")
45
45
  return
46
46
  }
47
- BGTaskScheduler.shared.cancel(taskIdentifier: taskId)
47
+ BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: taskId)
48
48
  callback(nil, nil)
49
49
 
50
50
  case "cancelAllTasks":
@@ -115,14 +115,6 @@ final class BluetoothModule: NativeModule {
115
115
  func invokeSync(method: String, args: [Any]) -> Any? { nil }
116
116
  }
117
117
 
118
- // MARK: - Safe array subscript
119
-
120
- private extension Array {
121
- subscript(safe index: Int) -> Element? {
122
- indices.contains(index) ? self[index] : nil
123
- }
124
- }
125
-
126
118
  // MARK: - BLE Manager
127
119
 
128
120
  @MainActor
@@ -151,11 +151,4 @@ final class CalendarModule: NativeModule {
151
151
  }
152
152
  }
153
153
 
154
- // MARK: - Safe array subscript (shared)
155
-
156
- private extension Array {
157
- subscript(safe index: Int) -> Element? {
158
- indices.contains(index) ? self[index] : nil
159
- }
160
- }
161
154
  #endif
@@ -178,13 +178,7 @@ final class CameraModule: NativeModule {
178
178
  // MARK: - Helpers
179
179
 
180
180
  private func topViewController() -> UIViewController? {
181
- guard let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene,
182
- let root = scene.windows.first(where: { $0.isKeyWindow })?.rootViewController else {
183
- return nil
184
- }
185
- var top = root
186
- while let presented = top.presentedViewController { top = presented }
187
- return top
181
+ return UIApplication.shared.vn_topViewController
188
182
  }
189
183
 
190
184
  private static var delegateKey: UInt8 = 0
@@ -271,16 +265,19 @@ private final class QRScanDelegate: NSObject, AVCaptureMetadataOutputObjectsDele
271
265
  let data = readable.stringValue else { return }
272
266
 
273
267
  let bounds = readable.bounds
274
- bridge?.dispatchGlobalEvent("camera:qrDetected", payload: [
275
- "data": data,
276
- "type": readable.type.rawValue,
277
- "bounds": [
278
- "x": bounds.origin.x,
279
- "y": bounds.origin.y,
280
- "width": bounds.size.width,
281
- "height": bounds.size.height,
282
- ],
283
- ])
268
+ let type = readable.type.rawValue
269
+ DispatchQueue.main.async { [weak self] in
270
+ self?.bridge?.dispatchGlobalEvent("camera:qrDetected", payload: [
271
+ "data": data,
272
+ "type": type,
273
+ "bounds": [
274
+ "x": bounds.origin.x,
275
+ "y": bounds.origin.y,
276
+ "width": bounds.size.width,
277
+ "height": bounds.size.height,
278
+ ],
279
+ ])
280
+ }
284
281
  }
285
282
  }
286
283
 
@@ -154,7 +154,7 @@ final class IAPModule: NativeModule {
154
154
  "transactionId": String(transaction.id),
155
155
  ]
156
156
  await MainActor.run {
157
- self.bridge?.emitGlobalEvent("iap:transactionUpdate", payload: payload)
157
+ self.bridge?.dispatchGlobalEvent("iap:transactionUpdate", payload: payload)
158
158
  }
159
159
  } catch {
160
160
  let payload: [String: Any] = [
@@ -163,7 +163,7 @@ final class IAPModule: NativeModule {
163
163
  "error": error.localizedDescription,
164
164
  ]
165
165
  await MainActor.run {
166
- self.bridge?.emitGlobalEvent("iap:transactionUpdate", payload: payload)
166
+ self.bridge?.dispatchGlobalEvent("iap:transactionUpdate", payload: payload)
167
167
  }
168
168
  }
169
169
  }
@@ -4,7 +4,7 @@ import UIKit
4
4
  /// Native module for performance profiling.
5
5
  /// Tracks FPS via CADisplayLink, memory usage via task_info, and bridge operation counts.
6
6
  /// Dispatches `perf:metrics` global events every 1 second while profiling is active.
7
- final class PerformanceModule: NativeModule {
7
+ final class PerformanceModule: NSObject, NativeModule {
8
8
 
9
9
  let moduleName = "Performance"
10
10
 
@@ -25,6 +25,7 @@ final class PerformanceModule: NativeModule {
25
25
 
26
26
  init(bridge: NativeBridge) {
27
27
  self.bridge = bridge
28
+ super.init()
28
29
  }
29
30
 
30
31
  func invoke(method: String, args: [Any], callback: @escaping (Any?, String?) -> Void) {
@@ -116,7 +117,9 @@ final class PerformanceModule: NativeModule {
116
117
  guard isProfiling else { return }
117
118
  bridgeOpsCount += 1 // Count the metrics dispatch itself
118
119
  let metrics = collectMetrics()
119
- bridge?.dispatchGlobalEvent("perf:metrics", payload: metrics)
120
+ DispatchQueue.main.async { [weak self] in
121
+ self?.bridge?.dispatchGlobalEvent("perf:metrics", payload: metrics)
122
+ }
120
123
  }
121
124
 
122
125
  // MARK: - Memory measurement
@@ -18,8 +18,7 @@ final class ShareModule: NativeModule {
18
18
 
19
19
  DispatchQueue.main.async {
20
20
  let vc = UIActivityViewController(activityItems: items, applicationActivities: nil)
21
- if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
22
- let rootVC = windowScene.windows.first?.rootViewController {
21
+ if let rootVC = UIApplication.shared.vn_keyWindow?.rootViewController {
23
22
  // iPad popover support
24
23
  if let popover = vc.popoverPresentationController {
25
24
  popover.sourceView = rootVC.view
@@ -114,9 +114,7 @@ final class SocialAuthModule: NSObject, NativeModule, ASAuthorizationControllerD
114
114
  }
115
115
 
116
116
  func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
117
- return UIApplication.shared.connectedScenes
118
- .compactMap { $0 as? UIWindowScene }
119
- .first?.windows.first { $0.isKeyWindow } ?? UIWindow()
117
+ return UIApplication.shared.vn_keyWindow ?? UIWindow()
120
118
  }
121
119
 
122
120
  // MARK: - Google Sign In (URL-based OAuth)
@@ -164,10 +162,11 @@ final class SocialAuthModule: NSObject, NativeModule, ASAuthorizationControllerD
164
162
 
165
163
  session.prefersEphemeralWebBrowserSession = false
166
164
 
167
- // Find the presentation anchor
168
- if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
169
- let window = windowScene.windows.first {
170
- session.presentationContextProvider = window.rootViewController as? ASWebAuthenticationPresentationContextProviding
165
+ // Provide a proper presentation context for the auth session
166
+ if let window = UIApplication.shared.vn_keyWindow {
167
+ let provider = WebAuthContextProvider(window: window)
168
+ objc_setAssociatedObject(session, &SocialAuthModule.webAuthProviderKey, provider, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
169
+ session.presentationContextProvider = provider
171
170
  }
172
171
 
173
172
  session.start()
@@ -227,14 +226,31 @@ final class SocialAuthModule: NSObject, NativeModule, ASAuthorizationControllerD
227
226
  queue: .main
228
227
  ) { [weak self] _ in
229
228
  UserDefaults.standard.removeObject(forKey: "vn_apple_userId")
230
- self?.bridge?.emitGlobalEvent("auth:appleCredentialRevoked", payload: [:])
229
+ self?.bridge?.dispatchGlobalEvent("auth:appleCredentialRevoked", payload: [:])
231
230
  }
232
231
  }
233
232
 
233
+ private static var webAuthProviderKey: UInt8 = 0
234
+
234
235
  deinit {
235
236
  if let observer = credentialObserver {
236
237
  NotificationCenter.default.removeObserver(observer)
237
238
  }
238
239
  }
239
240
  }
241
+
242
+ // MARK: - ASWebAuthenticationSession presentation context
243
+
244
+ private final class WebAuthContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding {
245
+ private let window: UIWindow
246
+
247
+ init(window: UIWindow) {
248
+ self.window = window
249
+ super.init()
250
+ }
251
+
252
+ func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
253
+ return window
254
+ }
255
+ }
240
256
  #endif
@@ -495,9 +495,9 @@ enum StyleEngine {
495
495
  case "direction":
496
496
  if let str = value as? String {
497
497
  switch str {
498
- case "ltr": flex.direction(.LTR)
499
- case "rtl": flex.direction(.RTL)
500
- case "inherit": flex.direction(.inherit)
498
+ case "ltr": flex.layoutDirection(.ltr)
499
+ case "rtl": flex.layoutDirection(.rtl)
500
+ case "inherit": flex.layoutDirection(.inherit)
501
501
  default: break
502
502
  }
503
503
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thelacanians/vue-native-cli",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "CLI for creating and running Vue Native apps",
5
5
  "license": "MIT",
6
6
  "author": "Vue Native Contributors",