pakstr 0.21.1 → 0.23.0

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 (20) hide show
  1. package/README.md +1 -1
  2. package/android-template/app/src/androidTest/java/com/pakstr/app/Nip55CallbackTest.kt +1 -1
  3. package/android-template/app/src/main/java/com/pakstr/app/LocalServer.kt +12 -28
  4. package/android-template/app/src/main/java/com/pakstr/app/Nip55Callback.kt +3 -0
  5. package/android-template/app/src/main/java/com/pakstr/app/Nip55SignEventRequest.kt +303 -0
  6. package/android-template/app/src/main/java/com/pakstr/app/PakstrBridge.kt +14 -2
  7. package/android-template/app/src/main/java/com/pakstr/app/RuntimeApiBaseSettings.kt +98 -0
  8. package/android-template/app/src/main/java/com/pakstr/app/RuntimeApiBaseValidator.kt +30 -0
  9. package/android-template/app/src/main/java/com/pakstr/app/RuntimeConfig.kt +7 -12
  10. package/android-template/app/src/main/java/com/pakstr/app/WebViewController.kt +67 -17
  11. package/android-template/app/src/main/java/com/pakstr/app/debug/DeveloperToolsActivity.kt +70 -4
  12. package/android-template/app/src/main/res/layout/activity_developer_tools.xml +257 -66
  13. package/android-template/app/src/main/res/values/strings.xml +24 -5
  14. package/android-template/app/src/test/java/com/pakstr/app/Nip55SignEventDispatchTest.kt +115 -0
  15. package/android-template/app/src/test/java/com/pakstr/app/Nip55SignEventRequestTest.kt +90 -0
  16. package/android-template/app/src/test/java/com/pakstr/app/PakstrBridgeRuntimeApiBaseTest.kt +52 -0
  17. package/android-template/app/src/test/java/com/pakstr/app/RuntimeApiBaseProxyTest.kt +49 -0
  18. package/android-template/app/src/test/java/com/pakstr/app/RuntimeApiBaseSettingsTest.kt +107 -0
  19. package/android-template/app/src/test/java/com/pakstr/app/RuntimeApiBaseValidatorTest.kt +50 -0
  20. package/package.json +1 -1
@@ -17,6 +17,13 @@ import com.pakstr.app.debug.AppDebugLogger
17
17
  import com.pakstr.app.permissions.interfaces.PermissionHandler
18
18
  import com.pakstr.app.signer.NostrBridge
19
19
  import com.pakstr.app.signer.SignerManager
20
+ import kotlinx.coroutines.CancellationException
21
+ import kotlinx.coroutines.CoroutineScope
22
+ import kotlinx.coroutines.Dispatchers
23
+ import kotlinx.coroutines.SupervisorJob
24
+ import kotlinx.coroutines.cancel
25
+ import kotlinx.coroutines.launch
26
+ import kotlinx.coroutines.withContext
20
27
 
21
28
  /**
22
29
  * Controls WebView configuration and lifecycle.
@@ -44,6 +51,7 @@ class WebViewController(
44
51
  ) {
45
52
  private var pendingNip55Result: String? = null
46
53
  private var isSetup = false
54
+ private val nip55Scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
47
55
 
48
56
  // Keeps a reference to the runnable to prevent memory leaks if destroyed early
49
57
  private val loadRunnable = Runnable {
@@ -228,22 +236,37 @@ class WebViewController(
228
236
  ): Boolean {
229
237
  val url = request?.url ?: return true
230
238
  if (url.scheme.equals("nostrsigner", ignoreCase = true)) {
231
- try {
232
- val intent = Intent(Intent.ACTION_VIEW, url).apply {
233
- putExtra(
234
- Browser.EXTRA_APPLICATION_ID,
235
- context.packageName
236
- )
237
- }
238
- context.startActivity(intent)
239
- } catch (error: ActivityNotFoundException) {
240
- AppDebugLogger.error(
241
- context,
242
- "WEBVIEW",
243
- "No app can handle nostrsigner URL",
244
- error
245
- )
246
- }
239
+ dispatchNip55SignEvent(
240
+ rawUrl = url.toString(),
241
+ activeSignerType = signerManager.getActiveType(),
242
+ expectedCallbackUrl = Nip55Callback.callbackPrefix,
243
+ forwardExternally = { launchExternalNip55(url) },
244
+ launchSigning = { operation ->
245
+ nip55Scope.launch {
246
+ try {
247
+ operation()
248
+ } catch (error: CancellationException) {
249
+ throw error
250
+ } catch (error: Exception) {
251
+ AppDebugLogger.log(
252
+ context,
253
+ "NIP55",
254
+ "Bunker flow failed: ${error::class.java.simpleName}"
255
+ )
256
+ }
257
+ }
258
+ },
259
+ getActivePubkey = signerManager::getCurrentUserPubkey,
260
+ signEvent = { event ->
261
+ signerManager.getSigner().signEvent(event)
262
+ },
263
+ deliverSignedEvent = { signedEvent ->
264
+ withContext(Dispatchers.Main) {
265
+ handleNip55Callback(signedEvent)
266
+ }
267
+ },
268
+ logRejection = ::logNip55Rejection
269
+ )
247
270
  return true
248
271
  }
249
272
  return url.host != "127.0.0.1"
@@ -316,7 +339,7 @@ class WebViewController(
316
339
 
317
340
  )
318
341
  webView.addJavascriptInterface(
319
- PakstrBridge(),
342
+ PakstrBridge(RuntimeApiBaseSettings.from(context)),
320
343
  "PakstrBridge"
321
344
  )
322
345
 
@@ -355,7 +378,34 @@ class WebViewController(
355
378
  (context as? Activity)?.findViewById<View>(R.id.loader)?.visibility = View.GONE
356
379
  }
357
380
 
381
+ private fun launchExternalNip55(url: android.net.Uri) {
382
+ try {
383
+ val intent = Intent(Intent.ACTION_VIEW, url).apply {
384
+ putExtra(
385
+ Browser.EXTRA_APPLICATION_ID,
386
+ context.packageName
387
+ )
388
+ }
389
+ context.startActivity(intent)
390
+ } catch (_: ActivityNotFoundException) {
391
+ AppDebugLogger.error(
392
+ context,
393
+ "WEBVIEW",
394
+ "No app can handle nostrsigner URL"
395
+ )
396
+ }
397
+ }
398
+
399
+ private fun logNip55Rejection(reason: String) {
400
+ AppDebugLogger.log(
401
+ context,
402
+ "NIP55",
403
+ "Request rejected: $reason"
404
+ )
405
+ }
406
+
358
407
  fun destroy() {
408
+ nip55Scope.cancel()
359
409
  webView.stopLoading()
360
410
 
361
411
  webView.clearHistory()
@@ -15,13 +15,18 @@ import android.widget.TextView
15
15
  import android.widget.Toast
16
16
  import androidx.activity.enableEdgeToEdge
17
17
  import androidx.appcompat.app.AppCompatActivity
18
+ import androidx.core.view.ViewCompat
19
+ import androidx.core.view.WindowInsetsCompat
20
+ import androidx.core.view.updatePadding
18
21
  import com.google.android.material.appbar.MaterialToolbar
19
22
  import com.google.android.material.button.MaterialButton
20
23
  import com.google.android.material.dialog.MaterialAlertDialogBuilder
21
24
  import com.google.android.material.switchmaterial.SwitchMaterial
25
+ import com.google.android.material.textfield.TextInputEditText
26
+ import com.google.android.material.textfield.TextInputLayout
22
27
  import com.pakstr.app.BuildConfig
23
28
  import com.pakstr.app.R
24
- import com.pakstr.app.RuntimeConfig
29
+ import com.pakstr.app.RuntimeApiBaseSettings
25
30
 
26
31
  class DeveloperToolsActivity : AppCompatActivity() {
27
32
 
@@ -30,8 +35,12 @@ class DeveloperToolsActivity : AppCompatActivity() {
30
35
  private lateinit var uptimeValue: TextView
31
36
  private lateinit var debugModeSwitch: SwitchMaterial
32
37
  private lateinit var apiSwitch: SwitchMaterial
38
+ private lateinit var apiBaseInputLayout: TextInputLayout
39
+ private lateinit var apiBaseInput: TextInputEditText
40
+ private lateinit var apiBaseSource: TextView
33
41
  private lateinit var debugWebViewStatus: TextView
34
42
  private lateinit var webViewStatus: TextView
43
+ private lateinit var runtimeApiBaseSettings: RuntimeApiBaseSettings
35
44
 
36
45
  private val uptimeUpdater = object : Runnable {
37
46
  override fun run() {
@@ -50,11 +59,14 @@ class DeveloperToolsActivity : AppCompatActivity() {
50
59
  }
51
60
 
52
61
  setContentView(R.layout.activity_developer_tools)
62
+ runtimeApiBaseSettings = RuntimeApiBaseSettings.from(this)
53
63
 
64
+ setupWindowInsets()
54
65
  setupToolbar()
55
66
  bindViews()
56
67
  populateRuntimeDetails()
57
68
  setupDebugControls()
69
+ setupApiBaseControls()
58
70
  setupLogActions()
59
71
  setupCrashTesting()
60
72
  setupWebViewActions()
@@ -71,6 +83,18 @@ class DeveloperToolsActivity : AppCompatActivity() {
71
83
  super.onStop()
72
84
  }
73
85
 
86
+ private fun setupWindowInsets() {
87
+ ViewCompat.setOnApplyWindowInsetsListener(
88
+ findViewById(R.id.developerToolsRoot)
89
+ ) { view, insets ->
90
+ val bars = insets.getInsets(
91
+ WindowInsetsCompat.Type.systemBars()
92
+ )
93
+ view.updatePadding(top = bars.top)
94
+ insets
95
+ }
96
+ }
97
+
74
98
  private fun setupToolbar() {
75
99
  findViewById<MaterialToolbar>(R.id.developerToolsToolbar).setNavigationOnClickListener {
76
100
  finish()
@@ -81,6 +105,9 @@ class DeveloperToolsActivity : AppCompatActivity() {
81
105
  uptimeValue = findViewById(R.id.runtimeUptimeValue)
82
106
  debugModeSwitch = findViewById(R.id.debugModeSwitch)
83
107
  apiSwitch = findViewById(R.id.apiEnabledSwitch)
108
+ apiBaseInputLayout = findViewById(R.id.apiBaseInputLayout)
109
+ apiBaseInput = findViewById(R.id.apiBaseInput)
110
+ apiBaseSource = findViewById(R.id.apiBaseSource)
84
111
  debugWebViewStatus = findViewById(R.id.debugWebViewStatus)
85
112
  webViewStatus = findViewById(R.id.webViewStatus)
86
113
  }
@@ -101,8 +128,6 @@ class DeveloperToolsActivity : AppCompatActivity() {
101
128
  }
102
129
 
103
130
  private fun setupDebugControls() {
104
- apiSwitch.isChecked = RuntimeConfig.apiEnabled(this)
105
-
106
131
  debugModeSwitch.setOnCheckedChangeListener { _, isChecked ->
107
132
  if (isChecked) {
108
133
  DeveloperToolsManager.enableDebug(this)
@@ -115,9 +140,50 @@ class DeveloperToolsActivity : AppCompatActivity() {
115
140
  }
116
141
  }
117
142
 
143
+ private fun setupApiBaseControls() {
144
+ findViewById<MaterialButton>(R.id.saveApiBaseButton).setOnClickListener {
145
+ val value = apiBaseInput.text?.toString().orEmpty()
146
+ if (runtimeApiBaseSettings.setOverride(value)) {
147
+ apiBaseInputLayout.error = null
148
+ refreshApiBase()
149
+ showMessage(R.string.developer_tools_api_base_saved)
150
+ } else {
151
+ apiBaseInputLayout.error = getString(
152
+ R.string.developer_tools_api_base_invalid
153
+ )
154
+ }
155
+ }
156
+ findViewById<MaterialButton>(R.id.resetApiBaseButton).setOnClickListener {
157
+ if (runtimeApiBaseSettings.reset()) {
158
+ apiBaseInputLayout.error = null
159
+ refreshApiBase()
160
+ showMessage(R.string.developer_tools_api_base_reset)
161
+ } else {
162
+ showMessage(R.string.developer_tools_api_base_save_failed)
163
+ }
164
+ }
165
+ refreshApiBase()
166
+ }
167
+
168
+ private fun refreshApiBase() {
169
+ val current = runtimeApiBaseSettings.current()
170
+ apiSwitch.isChecked = current.url != null
171
+ apiBaseInput.setText(current.url.orEmpty())
172
+ apiBaseSource.setText(
173
+ when (current.source) {
174
+ RuntimeApiBaseSettings.Source.RUNTIME_OVERRIDE ->
175
+ R.string.developer_tools_api_base_runtime_override
176
+ RuntimeApiBaseSettings.Source.PACKAGED_DEFAULT ->
177
+ R.string.developer_tools_api_base_packaged_default
178
+ RuntimeApiBaseSettings.Source.DISABLED ->
179
+ R.string.developer_tools_api_base_not_configured
180
+ }
181
+ )
182
+ }
183
+
118
184
  private fun refreshStatuses() {
119
185
  debugModeSwitch.isChecked = DeveloperToolsManager.isEnabled(this)
120
- apiSwitch.isChecked = RuntimeConfig.apiEnabled(this)
186
+ refreshApiBase()
121
187
 
122
188
  val status = if (isWebViewDebugEnabled()) {
123
189
  R.string.developer_tools_enabled