craft-native 0.0.55 → 0.0.72

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 (146) hide show
  1. package/dist/android/src/index.d.ts +73 -0
  2. package/dist/android/src/index.js +177 -37
  3. package/dist/android/templates/AndroidManifest.xml.template +10 -36
  4. package/dist/android/templates/CraftBridge.kt.template +192 -60
  5. package/dist/android/templates/CraftHealthConnect.kt.template +200 -0
  6. package/dist/android/templates/CraftHealthConnectStub.kt.template +28 -0
  7. package/dist/android/templates/LocationRecordingService.kt.template +160 -0
  8. package/dist/android/templates/MainActivity.kt.template +140 -19
  9. package/dist/android/templates/build.gradle.kts.app.template +23 -1
  10. package/dist/android/templates/build.gradle.kts.project.template +3 -2
  11. package/dist/android/templates/proguard-rules.pro.template +9 -0
  12. package/dist/api/android-advanced.d.ts +5 -6
  13. package/dist/api/app.d.ts +0 -1
  14. package/dist/api/clipboard.d.ts +0 -1
  15. package/dist/api/crypto.d.ts +1 -2
  16. package/dist/api/db.d.ts +1 -2
  17. package/dist/api/dialog.d.ts +0 -1
  18. package/dist/api/fs.d.ts +1 -2
  19. package/dist/api/gestures.d.ts +66 -0
  20. package/dist/api/http.d.ts +1 -2
  21. package/dist/api/index.d.ts +37 -34
  22. package/dist/api/ios-advanced.d.ts +6 -7
  23. package/dist/api/macos-advanced.d.ts +2 -3
  24. package/dist/api/media.d.ts +0 -1
  25. package/dist/api/mobile.d.ts +109 -1
  26. package/dist/api/process.d.ts +0 -1
  27. package/dist/api/sidebar.d.ts +3 -20
  28. package/dist/api/spaces.d.ts +83 -0
  29. package/dist/api/tray.d.ts +2 -3
  30. package/dist/api/window.d.ts +1 -2
  31. package/dist/api/windows-advanced.d.ts +6 -7
  32. package/dist/assets/index.d.ts +0 -1
  33. package/dist/binary-resolver.d.ts +23 -1
  34. package/dist/bridge/android.d.ts +0 -1
  35. package/dist/bridge/core.d.ts +0 -38
  36. package/dist/bridge/ids.d.ts +0 -1
  37. package/dist/bridge/ios.d.ts +0 -1
  38. package/dist/bridge/webkit-pending.d.ts +0 -1
  39. package/dist/build/bundle-optimizer.d.ts +0 -1
  40. package/dist/cli.js +666 -90
  41. package/dist/components/index.d.ts +6 -7
  42. package/dist/components/native.d.ts +0 -1
  43. package/dist/components/sidebar.d.ts +2 -81
  44. package/dist/dev/devtools.d.ts +0 -1
  45. package/dist/dev/error-overlay.d.ts +0 -1
  46. package/dist/dev/hot-reload.d.ts +0 -1
  47. package/dist/index.cjs +113 -3
  48. package/dist/index.d.cts +16 -16
  49. package/dist/index.d.ts +16 -16
  50. package/dist/index.js +112 -4
  51. package/dist/ios/src/index.d.ts +172 -0
  52. package/dist/ios/src/index.js +440 -42
  53. package/dist/ios/templates/CraftActivityAttributes.swift +14 -0
  54. package/dist/ios/templates/CraftApp.swift +1001 -73
  55. package/dist/ios/templates/CraftLiveActivityWidget.swift.template +62 -0
  56. package/dist/ios/templates/CraftWatchApp.swift.template +155 -0
  57. package/dist/ios/templates/Info.plist.template +10 -9
  58. package/dist/ios/templates/WatchApp.Info.plist.template +26 -0
  59. package/dist/ios/templates/WidgetExtension.Info.plist +31 -0
  60. package/dist/ios/templates/project.yml.template +12 -1
  61. package/dist/logging/index.d.ts +0 -1
  62. package/dist/mobile.d.ts +8 -0
  63. package/dist/mobile.js +755 -0
  64. package/dist/optimizations/index.d.ts +3 -4
  65. package/dist/package.d.ts +11 -1
  66. package/dist/performance/runtime.d.ts +0 -1
  67. package/dist/performance/startup.d.ts +0 -1
  68. package/dist/plugins/index.d.ts +0 -1
  69. package/dist/security/index.d.ts +0 -1
  70. package/dist/sidebar-types.d.ts +102 -0
  71. package/dist/signing/index.d.ts +0 -1
  72. package/dist/styles/headwind.d.ts +0 -1
  73. package/dist/styles/index.d.ts +9 -10
  74. package/dist/styles/sidebar-templates.d.ts +0 -1
  75. package/dist/styles/sidebars.d.ts +0 -1
  76. package/dist/types.d.ts +131 -1
  77. package/dist/updater/index.d.ts +0 -1
  78. package/dist/utils/audio.d.ts +0 -1
  79. package/dist/utils/index.d.ts +4 -5
  80. package/dist/utils/native.d.ts +1 -2
  81. package/dist/utils/react.d.ts +0 -1
  82. package/dist/utils/storage.d.ts +0 -1
  83. package/dist/utils/svelte.d.ts +0 -1
  84. package/dist/utils/timer.d.ts +0 -1
  85. package/dist/utils/vue.d.ts +0 -1
  86. package/package.json +32 -4
  87. package/dist/android/src/index.js.map +0 -10
  88. package/dist/api/android-advanced.d.ts.map +0 -1
  89. package/dist/api/app.d.ts.map +0 -1
  90. package/dist/api/clipboard.d.ts.map +0 -1
  91. package/dist/api/crypto.d.ts.map +0 -1
  92. package/dist/api/db.d.ts.map +0 -1
  93. package/dist/api/dialog.d.ts.map +0 -1
  94. package/dist/api/fs.d.ts.map +0 -1
  95. package/dist/api/http.d.ts.map +0 -1
  96. package/dist/api/index.d.ts.map +0 -1
  97. package/dist/api/ios-advanced.d.ts.map +0 -1
  98. package/dist/api/macos-advanced.d.ts.map +0 -1
  99. package/dist/api/media.d.ts.map +0 -1
  100. package/dist/api/mobile.d.ts.map +0 -1
  101. package/dist/api/process.d.ts.map +0 -1
  102. package/dist/api/sidebar.d.ts.map +0 -1
  103. package/dist/api/tray.d.ts.map +0 -1
  104. package/dist/api/window.d.ts.map +0 -1
  105. package/dist/api/windows-advanced.d.ts.map +0 -1
  106. package/dist/assets/index.d.ts.map +0 -1
  107. package/dist/binary-resolver.d.ts.map +0 -1
  108. package/dist/bridge/android.d.ts.map +0 -1
  109. package/dist/bridge/core.d.ts.map +0 -1
  110. package/dist/bridge/ids.d.ts.map +0 -1
  111. package/dist/bridge/ios.d.ts.map +0 -1
  112. package/dist/bridge/webkit-pending.d.ts.map +0 -1
  113. package/dist/build/bundle-optimizer.d.ts.map +0 -1
  114. package/dist/components/index.d.ts.map +0 -1
  115. package/dist/components/native.d.ts.map +0 -1
  116. package/dist/components/sidebar.d.ts.map +0 -1
  117. package/dist/dev/devtools.d.ts.map +0 -1
  118. package/dist/dev/error-overlay.d.ts.map +0 -1
  119. package/dist/dev/hot-reload.d.ts.map +0 -1
  120. package/dist/index.d.ts.map +0 -1
  121. package/dist/index.js.map +0 -45
  122. package/dist/ios/src/index.js.map +0 -10
  123. package/dist/logging/index.d.ts.map +0 -1
  124. package/dist/optimizations/index.d.ts.map +0 -1
  125. package/dist/package.d.ts.map +0 -1
  126. package/dist/package.test.d.ts +0 -2
  127. package/dist/package.test.d.ts.map +0 -1
  128. package/dist/performance/runtime.d.ts.map +0 -1
  129. package/dist/performance/startup.d.ts.map +0 -1
  130. package/dist/plugins/index.d.ts.map +0 -1
  131. package/dist/security/index.d.ts.map +0 -1
  132. package/dist/signing/index.d.ts.map +0 -1
  133. package/dist/styles/headwind.d.ts.map +0 -1
  134. package/dist/styles/index.d.ts.map +0 -1
  135. package/dist/styles/sidebar-templates.d.ts.map +0 -1
  136. package/dist/styles/sidebars.d.ts.map +0 -1
  137. package/dist/types.d.ts.map +0 -1
  138. package/dist/updater/index.d.ts.map +0 -1
  139. package/dist/utils/audio.d.ts.map +0 -1
  140. package/dist/utils/index.d.ts.map +0 -1
  141. package/dist/utils/native.d.ts.map +0 -1
  142. package/dist/utils/react.d.ts.map +0 -1
  143. package/dist/utils/storage.d.ts.map +0 -1
  144. package/dist/utils/svelte.d.ts.map +0 -1
  145. package/dist/utils/timer.d.ts.map +0 -1
  146. package/dist/utils/vue.d.ts.map +0 -1
@@ -0,0 +1,160 @@
1
+ package {{PACKAGE_NAME}}
2
+
3
+ import android.Manifest
4
+ import android.app.NotificationChannel
5
+ import android.app.NotificationManager
6
+ import android.app.Service
7
+ import android.content.Context
8
+ import android.content.Intent
9
+ import android.content.pm.PackageManager
10
+ import android.os.Build
11
+ import android.os.IBinder
12
+ import android.os.Looper
13
+ import androidx.core.app.NotificationCompat
14
+ import androidx.core.content.ContextCompat
15
+ import com.google.android.gms.location.LocationCallback
16
+ import com.google.android.gms.location.LocationRequest
17
+ import com.google.android.gms.location.LocationResult
18
+ import com.google.android.gms.location.LocationServices
19
+ import com.google.android.gms.location.Priority
20
+ import org.json.JSONArray
21
+ import org.json.JSONObject
22
+ import java.io.File
23
+
24
+ object CraftLocationRecordingStore {
25
+ private const val PREFS = "craft_location_recording"
26
+ private const val FILE = "craft-location-recording.jsonl"
27
+
28
+ private fun prefs(context: Context) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
29
+ private fun file(context: Context) = File(context.filesDir, FILE)
30
+
31
+ @Synchronized
32
+ fun start(context: Context, id: String, startedAt: Long) {
33
+ file(context).writeText("")
34
+ prefs(context).edit()
35
+ .putString("id", id)
36
+ .putBoolean("active", true)
37
+ .putBoolean("paused", false)
38
+ .putLong("startedAt", startedAt)
39
+ .apply()
40
+ }
41
+
42
+ @Synchronized
43
+ fun setPaused(context: Context, paused: Boolean) {
44
+ prefs(context).edit().putBoolean("paused", paused).apply()
45
+ }
46
+
47
+ @Synchronized
48
+ fun stop(context: Context) {
49
+ prefs(context).edit().putBoolean("active", false).putBoolean("paused", false).apply()
50
+ }
51
+
52
+ fun isActive(context: Context) = prefs(context).getBoolean("active", false)
53
+ fun isPaused(context: Context) = prefs(context).getBoolean("paused", false)
54
+
55
+ @Synchronized
56
+ fun append(context: Context, location: android.location.Location) {
57
+ if (!isActive(context) || isPaused(context)) return
58
+ val json = JSONObject().apply {
59
+ put("latitude", location.latitude)
60
+ put("longitude", location.longitude)
61
+ put("accuracy", location.accuracy)
62
+ put("altitude", location.altitude)
63
+ put("speed", location.speed)
64
+ put("heading", location.bearing)
65
+ put("timestamp", location.time)
66
+ }
67
+ file(context).appendText("$json\n")
68
+ }
69
+
70
+ @Synchronized
71
+ fun locations(context: Context): JSONArray {
72
+ val result = JSONArray()
73
+ val source = file(context)
74
+ if (!source.exists()) return result
75
+ source.forEachLine { line ->
76
+ if (line.isNotBlank()) runCatching { result.put(JSONObject(line)) }
77
+ }
78
+ return result
79
+ }
80
+
81
+ fun state(context: Context, includeLocations: Boolean = false): JSONObject {
82
+ val values = prefs(context)
83
+ val locations = locations(context)
84
+ return JSONObject().apply {
85
+ put("id", values.getString("id", null))
86
+ put("active", values.getBoolean("active", false))
87
+ put("paused", values.getBoolean("paused", false))
88
+ put("startedAt", values.getLong("startedAt", 0).takeIf { it > 0 } ?: JSONObject.NULL)
89
+ put("sampleCount", locations.length())
90
+ if (includeLocations) put("locations", locations)
91
+ }
92
+ }
93
+ }
94
+
95
+ class LocationRecordingService : Service() {
96
+ private val client by lazy { LocationServices.getFusedLocationProviderClient(this) }
97
+ private var callback: LocationCallback? = null
98
+
99
+ override fun onCreate() {
100
+ super.onCreate()
101
+ val manager = getSystemService(NotificationManager::class.java)
102
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
103
+ manager.createNotificationChannel(NotificationChannel(CHANNEL, "Activity recording", NotificationManager.IMPORTANCE_LOW))
104
+ }
105
+ val notification = NotificationCompat.Builder(this, CHANNEL)
106
+ .setSmallIcon(android.R.drawable.ic_menu_mylocation)
107
+ .setContentTitle("Recording your activity")
108
+ .setContentText("WildLoop is securely saving your route")
109
+ .setOngoing(true)
110
+ .setSilent(true)
111
+ .build()
112
+ startForeground(NOTIFICATION_ID, notification)
113
+ }
114
+
115
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
116
+ when (intent?.action) {
117
+ ACTION_STOP -> {
118
+ stopUpdates()
119
+ stopSelf()
120
+ return START_NOT_STICKY
121
+ }
122
+ else -> startUpdates()
123
+ }
124
+ return if (CraftLocationRecordingStore.isActive(this)) START_STICKY else START_NOT_STICKY
125
+ }
126
+
127
+ private fun startUpdates() {
128
+ if (callback != null || !CraftLocationRecordingStore.isActive(this)) return
129
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) return
130
+ val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5_000)
131
+ .setMinUpdateIntervalMillis(2_000)
132
+ .setMaxUpdateDelayMillis(10_000)
133
+ .build()
134
+ callback = object : LocationCallback() {
135
+ override fun onLocationResult(result: LocationResult) {
136
+ for (location in result.locations) CraftLocationRecordingStore.append(this@LocationRecordingService, location)
137
+ }
138
+ }
139
+ client.requestLocationUpdates(request, callback!!, Looper.getMainLooper())
140
+ }
141
+
142
+ private fun stopUpdates() {
143
+ callback?.let { client.removeLocationUpdates(it) }
144
+ callback = null
145
+ }
146
+
147
+ override fun onDestroy() {
148
+ stopUpdates()
149
+ super.onDestroy()
150
+ }
151
+
152
+ override fun onBind(intent: Intent?): IBinder? = null
153
+
154
+ companion object {
155
+ const val ACTION_START = "craft.location.START"
156
+ const val ACTION_STOP = "craft.location.STOP"
157
+ private const val CHANNEL = "craft_location_recording"
158
+ private const val NOTIFICATION_ID = 7001
159
+ }
160
+ }
@@ -2,18 +2,35 @@ package {{PACKAGE_NAME}}
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.os.Bundle
5
+ import android.content.Intent
6
+ import android.net.Uri
7
+ import android.webkit.WebResourceRequest
8
+ import android.webkit.WebResourceError
9
+ import android.webkit.WebResourceResponse
5
10
  import android.webkit.WebChromeClient
6
11
  import android.webkit.WebSettings
7
12
  import android.webkit.WebView
8
13
  import android.webkit.WebViewClient
9
14
  import androidx.appcompat.app.AppCompatActivity
10
15
  import androidx.core.view.WindowCompat
11
- import java.io.BufferedReader
12
- import java.io.InputStreamReader
16
+ import androidx.webkit.WebViewAssetLoader
17
+ import org.json.JSONObject
18
+ import java.io.FileNotFoundException
19
+ import java.net.URLConnection
13
20
 
14
21
  class MainActivity : AppCompatActivity() {
15
22
  private lateinit var webView: WebView
16
23
  private lateinit var craftBridge: CraftBridge
24
+ private val devServerUrl: String? by lazy { loadDevServerUrl() }
25
+ private val trustedOrigins: Set<String> by lazy { loadTrustedOrigins() }
26
+ private val hasBundledFallback: Boolean by lazy { configJson()?.optBoolean("hasBundledFallback", false) == true }
27
+ private val bundledAssetLoader: WebViewAssetLoader by lazy {
28
+ WebViewAssetLoader.Builder()
29
+ .setDomain(BUNDLED_APP_HOST)
30
+ .addPathHandler("/", RouteAwareAssetPathHandler())
31
+ .build()
32
+ }
33
+ private var loadedBundledFallback = false
17
34
 
18
35
  @SuppressLint("SetJavaScriptEnabled")
19
36
  override fun onCreate(savedInstanceState: Bundle?) {
@@ -25,7 +42,8 @@ class MainActivity : AppCompatActivity() {
25
42
  setContentView(R.layout.activity_main)
26
43
 
27
44
  webView = findViewById(R.id.webview)
28
- craftBridge = CraftBridge(this, webView)
45
+ craftBridge = CraftBridge(this, webView, CraftHealthConnect(this, webView))
46
+ handleIncomingDeepLink(intent, dispatch = false)
29
47
 
30
48
  setupWebView()
31
49
  loadContent()
@@ -36,10 +54,12 @@ class MainActivity : AppCompatActivity() {
36
54
  webView.settings.apply {
37
55
  javaScriptEnabled = true
38
56
  domStorageEnabled = true
39
- allowFileAccess = true
40
- allowContentAccess = true
57
+ allowFileAccess = false
58
+ allowContentAccess = false
59
+ allowFileAccessFromFileURLs = false
60
+ allowUniversalAccessFromFileURLs = false
41
61
  mediaPlaybackRequiresUserGesture = false
42
- mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
62
+ mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
43
63
  cacheMode = WebSettings.LOAD_DEFAULT
44
64
  setSupportZoom(false)
45
65
  builtInZoomControls = false
@@ -49,7 +69,28 @@ class MainActivity : AppCompatActivity() {
49
69
  webView.webViewClient = object : WebViewClient() {
50
70
  override fun onPageFinished(view: WebView?, url: String?) {
51
71
  super.onPageFinished(view, url)
52
- craftBridge.injectBridge()
72
+ if (url != null && isTrustedUrl(url)) craftBridge.injectBridge()
73
+ }
74
+
75
+ override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
76
+ val url = request?.url ?: return null
77
+ return bundledAssetLoader.shouldInterceptRequest(url)
78
+ ?: super.shouldInterceptRequest(view, request)
79
+ }
80
+
81
+ override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
82
+ val url = request?.url?.toString() ?: return true
83
+ if (isTrustedUrl(url)) return false
84
+ runCatching { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) }
85
+ return true
86
+ }
87
+
88
+ override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
89
+ super.onReceivedError(view, request, error)
90
+ if (request?.isForMainFrame == true && hasBundledFallback && !loadedBundledFallback) {
91
+ loadedBundledFallback = true
92
+ webView.loadUrl(BUNDLED_APP_URL)
93
+ }
53
94
  }
54
95
  }
55
96
 
@@ -61,29 +102,97 @@ class MainActivity : AppCompatActivity() {
61
102
 
62
103
  private fun loadContent() {
63
104
  // Check for dev server URL in config
64
- val devServerUrl = getDevServerUrl()
65
- if (devServerUrl != null) {
66
- webView.loadUrl(devServerUrl)
105
+ val remoteUrl = devServerUrl
106
+ if (remoteUrl != null) {
107
+ webView.loadUrl(remoteUrl)
67
108
  } else {
68
109
  // Load from assets
69
- webView.loadUrl("file:///android_asset/index.html")
110
+ webView.loadUrl(BUNDLED_APP_URL)
111
+ }
112
+ }
113
+
114
+ private inner class RouteAwareAssetPathHandler : WebViewAssetLoader.PathHandler {
115
+ override fun handle(path: String): WebResourceResponse? {
116
+ val cleanPath = Uri.decode(path).substringBefore('?').trimStart('/')
117
+ if (cleanPath.split('/').any { it == ".." }) return null
118
+ val candidates = when {
119
+ cleanPath.isBlank() -> listOf("index.html")
120
+ cleanPath.endsWith('/') -> listOf("${cleanPath}index.html")
121
+ cleanPath.substringAfterLast('/').contains('.') -> listOf(cleanPath)
122
+ else -> listOf("${cleanPath}.html", "${cleanPath}/index.html")
123
+ }
124
+
125
+ for (candidate in candidates) {
126
+ try {
127
+ val stream = assets.open(candidate)
128
+ val mimeType = URLConnection.guessContentTypeFromName(candidate)
129
+ ?: when (candidate.substringAfterLast('.', "").lowercase()) {
130
+ "js", "mjs" -> "text/javascript"
131
+ "css" -> "text/css"
132
+ "json", "webmanifest" -> "application/json"
133
+ "svg" -> "image/svg+xml"
134
+ else -> "application/octet-stream"
135
+ }
136
+ val encoding = if (mimeType.startsWith("text/") || mimeType.contains("javascript") || mimeType.contains("json")) "UTF-8" else null
137
+ return WebResourceResponse(mimeType, encoding, stream)
138
+ } catch (_: FileNotFoundException) {
139
+ // Try the next clean-route candidate.
140
+ }
141
+ }
142
+ return null
143
+ }
144
+ }
145
+
146
+ private fun handleIncomingDeepLink(source: Intent?, dispatch: Boolean) {
147
+ val url = source?.data?.toString() ?: return
148
+ craftBridge.setInitialURL(url)
149
+ if (dispatch) craftBridge.dispatchDeepLink(url)
150
+ }
151
+
152
+ private fun configJson(): JSONObject? {
153
+ return try {
154
+ assets.open("craft.config.json").bufferedReader().use { JSONObject(it.readText()) }
155
+ } catch (_: Exception) {
156
+ null
70
157
  }
71
158
  }
72
159
 
73
- private fun getDevServerUrl(): String? {
160
+ private fun loadDevServerUrl(): String? {
74
161
  return try {
75
- val inputStream = assets.open("craft.config.json")
76
- val reader = BufferedReader(InputStreamReader(inputStream))
77
- val content = reader.readText()
78
- reader.close()
79
- // Simple JSON parsing for devServerURL
80
- val regex = "\"devServerURL\"\\s*:\\s*\"([^\"]+)\"".toRegex()
81
- regex.find(content)?.groupValues?.get(1)
162
+ configJson()?.optString("devServerURL")?.takeIf { it.isNotBlank() }
82
163
  } catch (e: Exception) {
83
164
  null
84
165
  }
85
166
  }
86
167
 
168
+ private fun loadTrustedOrigins(): Set<String> {
169
+ val origins = mutableSetOf<String>()
170
+ val config = configJson()
171
+ val configured = config?.optJSONArray("trustedOrigins")
172
+ if (configured != null) {
173
+ for (index in 0 until configured.length()) origins.add(configured.optString(index))
174
+ }
175
+ devServerUrl?.let { url ->
176
+ val uri = Uri.parse(url)
177
+ origins.add("${uri.scheme}://${uri.authority}")
178
+ }
179
+ return origins
180
+ }
181
+
182
+ private fun isTrustedUrl(value: String): Boolean {
183
+ val uri = Uri.parse(value)
184
+ if (uri.scheme == "https" && uri.host == BUNDLED_APP_HOST) return true
185
+ val origin = "${uri.scheme}://${uri.authority}"
186
+ val localDevelopment = uri.scheme == "http" && uri.host in setOf("localhost", "127.0.0.1", "10.0.2.2")
187
+ return (uri.scheme == "https" || localDevelopment) && origin in trustedOrigins
188
+ }
189
+
190
+ override fun onNewIntent(intent: Intent) {
191
+ super.onNewIntent(intent)
192
+ setIntent(intent)
193
+ handleIncomingDeepLink(intent, dispatch = true)
194
+ }
195
+
87
196
  override fun onBackPressed() {
88
197
  if (webView.canGoBack()) {
89
198
  webView.goBack()
@@ -103,7 +212,19 @@ class MainActivity : AppCompatActivity() {
103
212
  }
104
213
 
105
214
  override fun onDestroy() {
215
+ craftBridge.close()
106
216
  webView.destroy()
107
217
  super.onDestroy()
108
218
  }
219
+
220
+ @Deprecated("Delegates the Health Connect permission contract on Android 13 and earlier")
221
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
222
+ if (craftBridge.onActivityResult(requestCode, resultCode, data)) return
223
+ super.onActivityResult(requestCode, resultCode, data)
224
+ }
225
+
226
+ companion object {
227
+ private const val BUNDLED_APP_HOST = "appassets.androidplatform.net"
228
+ private const val BUNDLED_APP_URL = "https://appassets.androidplatform.net/"
229
+ }
109
230
  }
@@ -1,11 +1,12 @@
1
1
  plugins {
2
2
  id("com.android.application")
3
3
  id("org.jetbrains.kotlin.android")
4
+ {{GOOGLE_SERVICES_PLUGIN}}
4
5
  }
5
6
 
6
7
  android {
7
8
  namespace = "{{PACKAGE_NAME}}"
8
- compileSdk = {{TARGET_SDK}}
9
+ compileSdk = {{COMPILE_SDK}}
9
10
 
10
11
  defaultConfig {
11
12
  applicationId = "{{PACKAGE_NAME}}"
@@ -43,12 +44,21 @@ android {
43
44
  buildFeatures {
44
45
  viewBinding = true
45
46
  }
47
+
48
+ // Android's default AAPT ignore pattern drops underscore-prefixed asset
49
+ // directories. STX intentionally emits its browser runtime and generated
50
+ // styles under `_stx`, so keep those files in the APK.
51
+ androidResources {
52
+ ignoreAssetsPattern = "!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~"
53
+ }
46
54
  }
47
55
 
48
56
  dependencies {
49
57
  // AndroidX Core
50
58
  implementation("androidx.core:core-ktx:1.12.0")
51
59
  implementation("androidx.appcompat:appcompat:1.6.1")
60
+ implementation("androidx.fragment:fragment-ktx:1.6.2")
61
+ implementation("androidx.webkit:webkit:1.12.1")
52
62
  implementation("com.google.android.material:material:1.11.0")
53
63
  implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
54
64
 
@@ -69,6 +79,18 @@ dependencies {
69
79
  implementation("com.google.android.play:review:2.0.1")
70
80
  implementation("com.google.android.play:review-ktx:2.0.1")
71
81
 
82
+ // Native push notifications (included only when enabled)
83
+ {{FIREBASE_MESSAGING_DEPENDENCY}}
84
+
85
+ // Android Health Connect (included only when enabled)
86
+ {{HEALTH_CONNECT_DEPENDENCIES}}
87
+
88
+ // On-device computer vision APIs exposed by CraftBridge
89
+ implementation("com.google.mlkit:barcode-scanning:17.3.0")
90
+ implementation("com.google.mlkit:image-labeling:17.0.9")
91
+ implementation("com.google.mlkit:object-detection:17.0.2")
92
+ implementation("com.google.mlkit:text-recognition:16.0.1")
93
+
72
94
  // Google Play Billing (In-App Purchase)
73
95
  implementation("com.android.billingclient:billing:6.1.0")
74
96
  implementation("com.android.billingclient:billing-ktx:6.1.0")
@@ -1,5 +1,6 @@
1
1
  // Top-level build file where you can add configuration options common to all sub-projects/modules.
2
2
  plugins {
3
- id("com.android.application") version "8.2.0" apply false
4
- id("org.jetbrains.kotlin.android") version "1.9.20" apply false
3
+ id("com.android.application") version "8.9.1" apply false
4
+ id("org.jetbrains.kotlin.android") version "2.0.21" apply false
5
+ {{GOOGLE_SERVICES_PLUGIN}}
5
6
  }
@@ -0,0 +1,9 @@
1
+ # Preserve the JavaScript bridge contract and its annotated methods when R8
2
+ # optimizes release builds. These classes are reached from WebView by name.
3
+ -keepattributes RuntimeVisibleAnnotations,RuntimeInvisibleAnnotations
4
+ -keepclassmembers class * {
5
+ @android.webkit.JavascriptInterface <methods>;
6
+ }
7
+ -keep class {{PACKAGE_NAME}}.CraftBridge { *; }
8
+ -keep class {{PACKAGE_NAME}}.CraftHealthConnect { *; }
9
+ -keep class {{PACKAGE_NAME}}.LocationRecordingService { *; }
@@ -82,7 +82,7 @@ export declare const materialYou: {
82
82
  /**
83
83
  * Get colors from a specific source.
84
84
  */
85
- getColorsFromSource(source: "wallpaper" | "content"): Promise<MaterialYouColors | null>;
85
+ getColorsFromSource(source: 'wallpaper' | 'content'): Promise<MaterialYouColors | null>;
86
86
  /**
87
87
  * Listen for color changes.
88
88
  */
@@ -273,7 +273,7 @@ export declare const workManager: {
273
273
  /**
274
274
  * Enqueue unique work (replaces existing work with same name).
275
275
  */
276
- enqueueUnique(request: WorkRequest, existingWorkPolicy: "replace" | "keep" | "append" | "update"): Promise<string>;
276
+ enqueueUnique(request: WorkRequest, existingWorkPolicy: 'replace' | 'keep' | 'append' | 'update'): Promise<string>;
277
277
  /**
278
278
  * Cancel work by name.
279
279
  */
@@ -684,7 +684,7 @@ export declare const playBilling: {
684
684
  /**
685
685
  * Query product details.
686
686
  */
687
- getProducts(productIds: string[], productType: "inapp" | "subs"): Promise<PlayProduct[]>;
687
+ getProducts(productIds: string[], productType: 'inapp' | 'subs'): Promise<PlayProduct[]>;
688
688
  /**
689
689
  * Launch purchase flow.
690
690
  */
@@ -692,7 +692,7 @@ export declare const playBilling: {
692
692
  offerToken?: string;
693
693
  isOfferPersonalized?: boolean;
694
694
  oldPurchaseToken?: string;
695
- replacementMode?: "charge_prorated_price" | "charge_full_price" | "without_proration" | "deferred";
695
+ replacementMode?: 'charge_prorated_price' | 'charge_full_price' | 'without_proration' | 'deferred';
696
696
  }): Promise<{
697
697
  success: boolean;
698
698
  purchase?: PlayPurchase;
@@ -701,7 +701,7 @@ export declare const playBilling: {
701
701
  /**
702
702
  * Get purchases.
703
703
  */
704
- getPurchases(productType: "inapp" | "subs"): Promise<PlayPurchase[]>;
704
+ getPurchases(productType: 'inapp' | 'subs'): Promise<PlayPurchase[]>;
705
705
  /**
706
706
  * Acknowledge a purchase.
707
707
  */
@@ -734,4 +734,3 @@ declare const androidAdvanced: {
734
734
  playBilling: typeof playBilling;
735
735
  };
736
736
  export default androidAdvanced;
737
- //# sourceMappingURL=android-advanced.d.ts.map
package/dist/api/app.d.ts CHANGED
@@ -429,4 +429,3 @@ export declare const notify: (options: NotificationOptions) => Promise<void>;
429
429
  export declare const registerShortcut: (accelerator: string, handler: ShortcutHandler) => Promise<boolean>;
430
430
  export declare const unregisterShortcut: (accelerator: string) => Promise<void>;
431
431
  export default appManager;
432
- //# sourceMappingURL=app.d.ts.map
@@ -82,4 +82,3 @@ export declare const clipboard: {
82
82
  read: typeof read;
83
83
  };
84
84
  export default clipboard;
85
- //# sourceMappingURL=clipboard.d.ts.map
@@ -2,7 +2,7 @@
2
2
  * Craft Crypto API
3
3
  * Provides cryptographic operations through the Craft bridge
4
4
  */
5
- import type { CraftCryptoAPI } from '../types';
5
+ import type { CraftCryptoAPI } from '../types.js';
6
6
  /**
7
7
  * Error type for cryptographic failures (malformed input, decode errors, etc.).
8
8
  *
@@ -76,4 +76,3 @@ export declare function hashPassword(password: string, salt?: string): Promise<{
76
76
  */
77
77
  export declare function verifyPassword(password: string, hash: string, salt: string): Promise<boolean>;
78
78
  export default crypto;
79
- //# sourceMappingURL=crypto.d.ts.map
package/dist/api/db.d.ts CHANGED
@@ -26,7 +26,7 @@
26
26
  * ```
27
27
  */
28
28
  import { EventEmitter } from 'events';
29
- import type { CraftDatabaseAPI } from '../types';
29
+ import type { CraftDatabaseAPI } from '../types.js';
30
30
  /**
31
31
  * Global emitter for `db:execute` audit events. Subscribe to log every SQL
32
32
  * statement that goes through the SDK. The event payload is `{ name, sql,
@@ -424,4 +424,3 @@ export declare class KeyValueStore {
424
424
  clear(): Promise<void>;
425
425
  }
426
426
  export default db;
427
- //# sourceMappingURL=db.d.ts.map
@@ -138,4 +138,3 @@ export declare const dialog: {
138
138
  showPrompt: typeof showPrompt;
139
139
  };
140
140
  export default dialog;
141
- //# sourceMappingURL=dialog.d.ts.map
package/dist/api/fs.d.ts CHANGED
@@ -23,7 +23,7 @@
23
23
  * })
24
24
  * ```
25
25
  */
26
- import type { CraftFileSystemAPI } from '../types';
26
+ import type { CraftFileSystemAPI } from '../types.js';
27
27
  /**
28
28
  * Validate a file path to prevent directory traversal attacks.
29
29
  *
@@ -128,4 +128,3 @@ export declare function _resetWatchWarningForTests(): void;
128
128
  */
129
129
  export declare function watch(path: string, callback: (event: string, filename: string) => void, options?: WatchOptions): Promise<() => void>;
130
130
  export default fs;
131
- //# sourceMappingURL=fs.d.ts.map
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @fileoverview Trackpad gesture phases
3
+ * @description Real swipe begin/change/end from the host's `NSEvent`, rather
4
+ * than inferred from a phase-less `wheel` stream.
5
+ * @module @craft-native/api/gestures
6
+ */
7
+ export type SwipeAxis = 'horizontal' | 'vertical';
8
+ export type SwipePhase = 'begin' | 'change' | 'end';
9
+ export interface SwipeEvent {
10
+ /** Which way the gesture was locked at `begin`. Never changes mid-gesture. */
11
+ axis: SwipeAxis;
12
+ phase: SwipePhase;
13
+ /**
14
+ * Points since the previous event. Positive scrolls content left — i.e.
15
+ * advances to the next item — matching a DOM wheel event's `deltaX`.
16
+ */
17
+ deltaX: number;
18
+ deltaY: number;
19
+ /** Points per second at release. Only meaningful on `end`. */
20
+ velocityX: number;
21
+ /**
22
+ * True once the fingers have lifted and the OS is coasting. The macOS host
23
+ * drops momentum entirely so a swipe settles at finger-up, so this is
24
+ * currently always `false` there; it exists for hosts that choose to forward
25
+ * the tail.
26
+ */
27
+ momentum: boolean;
28
+ }
29
+ export type SwipeListener = (swipe: SwipeEvent) => void;
30
+ export type Unsubscribe = () => void;
31
+ export interface GesturesAPI {
32
+ onSwipe: (listener: SwipeListener) => Unsubscribe;
33
+ }
34
+ /**
35
+ * Whether the host forwards real gesture phases.
36
+ *
37
+ * Note this is `true` as soon as the registry is installed, which happens in
38
+ * every Craft window — it does **not** promise that a swipe will ever arrive.
39
+ * A host that never emits leaves the registry dormant, which is deliberate:
40
+ * callers keep their own `wheel` fallback bound and let the native stream
41
+ * supersede it on the first `begin` it delivers.
42
+ */
43
+ export declare function hasNativeGestures(): boolean;
44
+ /**
45
+ * Subscribe to swipe phases. Returns an unsubscribe function; calling it more
46
+ * than once is safe.
47
+ *
48
+ * In a plain browser this is a no-op returning a no-op, so it is safe to call
49
+ * unconditionally.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * const off = onSwipe((swipe) => {
54
+ * if (swipe.axis !== 'horizontal') return
55
+ * if (swipe.phase === 'begin') beginDrag()
56
+ * else if (swipe.phase === 'end') settle(swipe.velocityX)
57
+ * else track(swipe.deltaX)
58
+ * })
59
+ * ```
60
+ */
61
+ export declare function onSwipe(listener: SwipeListener): Unsubscribe;
62
+ /** Namespace form, for `import { gestures } from 'craft-native'`. */
63
+ export declare const gestures: {
64
+ hasNativeGestures: () => boolean;
65
+ onSwipe: (listener: SwipeListener) => Unsubscribe;
66
+ };
@@ -3,7 +3,7 @@
3
3
  * Provides native HTTP client through the Craft bridge
4
4
  * Bypasses CORS restrictions when running in Craft
5
5
  */
6
- import type { CraftHttpAPI } from '../types';
6
+ import type { CraftHttpAPI } from '../types.js';
7
7
  /**
8
8
  * Result of {@link encodeRequestBody}. `kind` lets the caller decide
9
9
  * whether to default a `Content-Type: application/json` header.
@@ -193,4 +193,3 @@ export interface WebSocketOptions {
193
193
  */
194
194
  export declare function createClient(options?: HttpClientOptions): HttpClient;
195
195
  export default http;
196
- //# sourceMappingURL=http.d.ts.map