mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-19 10:51:19 +00:00
Fix Android Amber signer integration and cleanup repo structure
- Added Amber external signer integration for secure private key management on Android - Fixed authentication issues and NIP-55 protocol implementation - Added comprehensive documentation in amber-integration-fixes.md - Moved android_backup to external location to keep repo clean - Updated .gitignore to exclude APK files
This commit is contained in:
parent
aad1b875a3
commit
8e68fcf60f
5
.gitignore
vendored
5
.gitignore
vendored
@ -18,7 +18,8 @@ web-build/
|
|||||||
|
|
||||||
# Prebuild folders - addressing workflow conflict
|
# Prebuild folders - addressing workflow conflict
|
||||||
# For CNG/Prebuild with EAS Build
|
# For CNG/Prebuild with EAS Build
|
||||||
/android
|
# Temporarily allowing Android files for EAS build
|
||||||
|
# /android
|
||||||
/ios
|
/ios
|
||||||
|
|
||||||
# Metro
|
# Metro
|
||||||
@ -44,3 +45,5 @@ yarn-error.*
|
|||||||
|
|
||||||
expo-env.d.ts
|
expo-env.d.ts
|
||||||
# @end expo-cli
|
# @end expo-cli
|
||||||
|
# Exclude build APK files
|
||||||
|
*.apk
|
||||||
|
15
CHANGELOG.md
15
CHANGELOG.md
@ -1,4 +1,8 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
All notable changes to the POWR project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
@ -10,8 +14,15 @@
|
|||||||
- Exposed new authentication method through useNDKAuth hook
|
- Exposed new authentication method through useNDKAuth hook
|
||||||
- Added "Sign with Amber" option to login screen
|
- Added "Sign with Amber" option to login screen
|
||||||
- Added comprehensive documentation in docs/technical/nostr/external-signers.md
|
- Added comprehensive documentation in docs/technical/nostr/external-signers.md
|
||||||
|
- Added technical documentation in docs/technical/nostr/amber-integration-fixes.md
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- Android: Fixed Amber external signer integration issues
|
||||||
|
- Added extensive logging to better diagnose communication issues
|
||||||
|
- Improved error handling in `AmberSignerModule.kt`
|
||||||
|
- Fixed intent construction to better follow NIP-55 protocol
|
||||||
|
- Enhanced response handling with checks for URI parameters and intent extras
|
||||||
|
- Added POWR-specific event kinds (1301, 33401, 33402) to permission requests
|
||||||
- Authentication state management issues
|
- Authentication state management issues
|
||||||
- Fixed hook ordering inconsistencies when switching between authenticated and unauthenticated states
|
- Fixed hook ordering inconsistencies when switching between authenticated and unauthenticated states
|
||||||
- Enhanced profile overview screen with consistent hook calling patterns
|
- Enhanced profile overview screen with consistent hook calling patterns
|
||||||
@ -30,10 +41,6 @@
|
|||||||
- TestFlight preparation: Hid development-only Programs tab in production builds
|
- TestFlight preparation: Hid development-only Programs tab in production builds
|
||||||
- TestFlight preparation: Removed debug UI and console logs from social feed in production builds
|
- TestFlight preparation: Removed debug UI and console logs from social feed in production builds
|
||||||
|
|
||||||
All notable changes to the POWR project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
||||||
|
|
||||||
# Changelog - March 28, 2025
|
# Changelog - March 28, 2025
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
@ -10,11 +12,21 @@
|
|||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
<data android:scheme="https"/>
|
<data android:scheme="https"/>
|
||||||
</intent>
|
</intent>
|
||||||
|
<!-- Add query for nostrsigner scheme used by Amber -->
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<data android:scheme="nostrsigner"/>
|
||||||
|
</intent>
|
||||||
|
<!-- Add specific package query for Amber -->
|
||||||
|
<package android:name="com.greenart7c3.nostrsigner"/>
|
||||||
</queries>
|
</queries>
|
||||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true">
|
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:fullBackupContent="@xml/secure_store_backup_rules" android:dataExtractionRules="@xml/secure_store_data_extraction_rules">
|
||||||
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
|
<meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/>
|
||||||
|
<meta-data android:name="expo.modules.updates.EXPO_RUNTIME_VERSION" android:value="@string/expo_runtime_version"/>
|
||||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
||||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
|
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
|
||||||
|
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://u.expo.dev/f3895f49-d9c9-4653-b73b-356f727debe2"/>
|
||||||
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
|
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
@ -26,6 +38,13 @@
|
|||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
<data android:scheme="myapp"/>
|
<data android:scheme="myapp"/>
|
||||||
<data android:scheme="com.powr.app"/>
|
<data android:scheme="com.powr.app"/>
|
||||||
|
<data android:scheme="exp+powr"/>
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter android:autoVerify="true" data-generated="true">
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<data android:scheme="powr"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
280
android/app/src/main/java/com/powr/app/AmberSignerModule.kt
Normal file
280
android/app/src/main/java/com/powr/app/AmberSignerModule.kt
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
package com.powr.app
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import com.facebook.react.bridge.*
|
||||||
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AmberSignerModule - React Native module that provides interfaces to communicate with Amber signer
|
||||||
|
* Implements NIP-55 for Android: https://github.com/nostr-protocol/nips/blob/master/55.md
|
||||||
|
*/
|
||||||
|
class AmberSignerModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext), ActivityEventListener {
|
||||||
|
private val TAG = "AmberSignerModule"
|
||||||
|
private var pendingPromise: Promise? = null
|
||||||
|
private val AMBER_PACKAGE_NAME = "com.greenart7c3.nostrsigner"
|
||||||
|
private val NOSTRSIGNER_SCHEME = "nostrsigner:"
|
||||||
|
|
||||||
|
init {
|
||||||
|
reactContext.addActivityEventListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "AmberSignerModule"
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun isExternalSignerInstalled(promise: Promise) {
|
||||||
|
val context = reactApplicationContext
|
||||||
|
val intent = Intent().apply {
|
||||||
|
action = Intent.ACTION_VIEW
|
||||||
|
data = Uri.parse(NOSTRSIGNER_SCHEME)
|
||||||
|
}
|
||||||
|
val infos = context.packageManager.queryIntentActivities(intent, 0)
|
||||||
|
Log.d(TAG, "External signer installed: ${infos.size > 0}")
|
||||||
|
promise.resolve(infos.size > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun requestPublicKey(permissions: ReadableArray?, promise: Promise) {
|
||||||
|
Log.d(TAG, "requestPublicKey called with permissions: $permissions")
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(NOSTRSIGNER_SCHEME))
|
||||||
|
intent.`package` = AMBER_PACKAGE_NAME
|
||||||
|
intent.putExtra("type", "get_public_key")
|
||||||
|
|
||||||
|
// Convert permissions to JSON if provided
|
||||||
|
if (permissions != null) {
|
||||||
|
intent.putExtra("permissions", convertPermissionsToJson(permissions))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
pendingPromise = promise
|
||||||
|
val activity = currentActivity
|
||||||
|
if (activity != null) {
|
||||||
|
Log.d(TAG, "Starting activity for result")
|
||||||
|
activity.startActivityForResult(intent, REQUEST_CODE_SIGN)
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Activity doesn't exist")
|
||||||
|
promise.reject("E_ACTIVITY_DOES_NOT_EXIST", "Activity doesn't exist")
|
||||||
|
pendingPromise = null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error launching Amber activity: ${e.message}")
|
||||||
|
promise.reject("E_LAUNCH_ERROR", "Error launching Amber: ${e.message}")
|
||||||
|
pendingPromise = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun signEvent(eventJson: String, currentUserPubkey: String, eventId: String?, promise: Promise) {
|
||||||
|
Log.d(TAG, "signEvent called - eventJson length: ${eventJson.length}, npub: $currentUserPubkey")
|
||||||
|
|
||||||
|
// For NIP-55, we need to create a URI with the event JSON
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(NOSTRSIGNER_SCHEME))
|
||||||
|
intent.`package` = AMBER_PACKAGE_NAME
|
||||||
|
intent.putExtra("type", "sign_event")
|
||||||
|
intent.putExtra("event", eventJson) // Event data as extra instead of URI
|
||||||
|
|
||||||
|
// Add event ID for tracking (optional but useful)
|
||||||
|
if (eventId != null) {
|
||||||
|
intent.putExtra("id", eventId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the current logged in user npub
|
||||||
|
intent.putExtra("current_user", currentUserPubkey)
|
||||||
|
|
||||||
|
// Add flags to handle multiple signing requests
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
|
|
||||||
|
try {
|
||||||
|
pendingPromise = promise
|
||||||
|
val activity = currentActivity
|
||||||
|
if (activity != null) {
|
||||||
|
Log.d(TAG, "Starting activity for result")
|
||||||
|
activity.startActivityForResult(intent, REQUEST_CODE_SIGN)
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Activity doesn't exist")
|
||||||
|
promise.reject("E_ACTIVITY_DOES_NOT_EXIST", "Activity doesn't exist")
|
||||||
|
pendingPromise = null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error launching Amber activity: ${e.message}")
|
||||||
|
promise.reject("E_LAUNCH_ERROR", "Error launching Amber: ${e.message}")
|
||||||
|
pendingPromise = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert ReadableArray of permissions to JSON string
|
||||||
|
private fun convertPermissionsToJson(permissions: ReadableArray): String {
|
||||||
|
val jsonArray = JSONArray()
|
||||||
|
for (i in 0 until permissions.size()) {
|
||||||
|
val permission = permissions.getMap(i)
|
||||||
|
val jsonPermission = JSONObject()
|
||||||
|
jsonPermission.put("type", permission.getString("type"))
|
||||||
|
if (permission.hasKey("kind")) {
|
||||||
|
jsonPermission.put("kind", permission.getInt("kind"))
|
||||||
|
}
|
||||||
|
jsonArray.put(jsonPermission)
|
||||||
|
}
|
||||||
|
return jsonArray.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
Log.d(TAG, "onActivityResult - requestCode: $requestCode, resultCode: $resultCode, data: $data")
|
||||||
|
|
||||||
|
if (requestCode == REQUEST_CODE_SIGN) {
|
||||||
|
val promise = pendingPromise
|
||||||
|
pendingPromise = null
|
||||||
|
|
||||||
|
if (promise == null) {
|
||||||
|
Log.w(TAG, "No pending promise for activity result")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultCode != Activity.RESULT_OK) {
|
||||||
|
Log.e(TAG, "Activity result not OK: $resultCode")
|
||||||
|
promise.reject("E_CANCELLED", "User cancelled or activity failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data == null) {
|
||||||
|
Log.e(TAG, "No data returned from activity")
|
||||||
|
promise.reject("E_NO_DATA", "No data returned from Amber")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log all extras for debugging
|
||||||
|
val extras = data.extras
|
||||||
|
if (extras != null) {
|
||||||
|
Log.d(TAG, "Intent extras:")
|
||||||
|
for (key in extras.keySet()) {
|
||||||
|
Log.d(TAG, " $key: ${extras.get(key)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First try to get data from extras
|
||||||
|
val signature = data.getStringExtra("signature")
|
||||||
|
val id = data.getStringExtra("id")
|
||||||
|
val event = data.getStringExtra("event")
|
||||||
|
val packageName = data.getStringExtra("package")
|
||||||
|
|
||||||
|
val response = WritableNativeMap()
|
||||||
|
|
||||||
|
if (signature != null) {
|
||||||
|
response.putString("signature", signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id != null) {
|
||||||
|
response.putString("id", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event != null) {
|
||||||
|
response.putString("event", event)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packageName != null) {
|
||||||
|
response.putString("packageName", packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no extras, try to parse from URI
|
||||||
|
val uri = data.data
|
||||||
|
if (uri != null) {
|
||||||
|
// Check if we already have data from extras
|
||||||
|
if (signature == null) {
|
||||||
|
val uriSignature = uri.getQueryParameter("signature")
|
||||||
|
if (uriSignature != null) {
|
||||||
|
response.putString("signature", uriSignature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id == null) {
|
||||||
|
val uriId = uri.getQueryParameter("id")
|
||||||
|
if (uriId != null) {
|
||||||
|
response.putString("id", uriId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event == null) {
|
||||||
|
val uriEvent = uri.getQueryParameter("event")
|
||||||
|
if (uriEvent != null) {
|
||||||
|
response.putString("event", uriEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packageName == null) {
|
||||||
|
val uriPackage = uri.getQueryParameter("package")
|
||||||
|
if (uriPackage != null) {
|
||||||
|
response.putString("packageName", uriPackage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we received a JSON array of results
|
||||||
|
val uriSignature = uri.getQueryParameter("signature")
|
||||||
|
if (uriSignature?.startsWith("[") == true && uriSignature.endsWith("]")) {
|
||||||
|
try {
|
||||||
|
val resultsArray = JSONArray(uriSignature)
|
||||||
|
val results = WritableNativeArray()
|
||||||
|
|
||||||
|
for (i in 0 until resultsArray.length()) {
|
||||||
|
val resultObj = resultsArray.getJSONObject(i)
|
||||||
|
val resultMap = WritableNativeMap()
|
||||||
|
|
||||||
|
if (resultObj.has("signature")) {
|
||||||
|
resultMap.putString("signature", resultObj.getString("signature"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultObj.has("id")) {
|
||||||
|
resultMap.putString("id", resultObj.getString("id"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultObj.has("package")) {
|
||||||
|
val pkg = resultObj.optString("package")
|
||||||
|
if (pkg != "null") {
|
||||||
|
resultMap.putString("packageName", pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.pushMap(resultMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.putArray("results", results)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to parse JSON array result: ${e.message}")
|
||||||
|
// Continue with normal handling if parsing fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we got any data
|
||||||
|
if (!response.hasKey("signature") && !response.hasKey("results")) {
|
||||||
|
Log.e(TAG, "No signature data in response")
|
||||||
|
promise.reject("E_NO_DATA", "No signature data returned from Amber")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log full response for debugging
|
||||||
|
Log.d(TAG, "Amber response: $response")
|
||||||
|
promise.resolve(response)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error processing Amber response: ${e.message}")
|
||||||
|
promise.reject("E_PROCESSING_ERROR", "Error processing Amber response: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
// Not used for our implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val REQUEST_CODE_SIGN = 9001
|
||||||
|
}
|
||||||
|
}
|
22
android/app/src/main/java/com/powr/app/AmberSignerPackage.kt
Normal file
22
android/app/src/main/java/com/powr/app/AmberSignerPackage.kt
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package com.powr.app
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import com.facebook.react.ReactPackage
|
||||||
|
import com.facebook.react.bridge.NativeModule
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext
|
||||||
|
import com.facebook.react.uimanager.ReactShadowNode
|
||||||
|
import com.facebook.react.uimanager.ViewManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ReactPackage implementation for AmberSigner module
|
||||||
|
* This package is registered in MainApplication.kt
|
||||||
|
*/
|
||||||
|
class AmberSignerPackage : ReactPackage {
|
||||||
|
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
||||||
|
return listOf(AmberSignerModule(reactContext))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<View, ReactShadowNode<*>>> {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import android.app.Application
|
|||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
|
||||||
import com.facebook.react.PackageList
|
import com.facebook.react.PackageList
|
||||||
|
import com.powr.app.AmberSignerPackage
|
||||||
import com.facebook.react.ReactApplication
|
import com.facebook.react.ReactApplication
|
||||||
import com.facebook.react.ReactNativeHost
|
import com.facebook.react.ReactNativeHost
|
||||||
import com.facebook.react.ReactPackage
|
import com.facebook.react.ReactPackage
|
||||||
@ -23,8 +24,9 @@ class MainApplication : Application(), ReactApplication {
|
|||||||
object : DefaultReactNativeHost(this) {
|
object : DefaultReactNativeHost(this) {
|
||||||
override fun getPackages(): List<ReactPackage> {
|
override fun getPackages(): List<ReactPackage> {
|
||||||
val packages = PackageList(this).packages
|
val packages = PackageList(this).packages
|
||||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
// Packages that cannot be autolinked yet can be added manually here for example:
|
||||||
// packages.add(new MyReactNativePackage());
|
// packages.add(new MyReactNativePackage());
|
||||||
|
packages.add(AmberSignerPackage())
|
||||||
return packages
|
return packages
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,7 +46,7 @@ class MainApplication : Application(), ReactApplication {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
SoLoader.init(this, OpenSourceMergedSoMapping)
|
SoLoader.init(this, OpenSourceMergedSoMapping)
|
||||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
// If you opted-in for the New Architecture we load the native entry point for this app.
|
||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
ApplicationLifecycleDispatcher.onApplicationCreate(this)
|
ApplicationLifecycleDispatcher.onApplicationCreate(this)
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
// components/sheets/NostrLoginSheet.tsx
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { View, ActivityIndicator, Modal, TouchableOpacity, Platform } from 'react-native';
|
import { View, ActivityIndicator, Modal, TouchableOpacity, Platform } from 'react-native';
|
||||||
import { Text } from '@/components/ui/text';
|
import { Text } from '@/components/ui/text';
|
||||||
@ -83,29 +82,50 @@ export default function NostrLoginSheet({ open, onClose }: NostrLoginSheetProps)
|
|||||||
try {
|
try {
|
||||||
console.log('Attempting to login with Amber...');
|
console.log('Attempting to login with Amber...');
|
||||||
|
|
||||||
try {
|
// Define default permissions to request
|
||||||
// Request public key from Amber
|
const defaultPermissions = [
|
||||||
// This will throw an error because the native module isn't implemented
|
{ type: 'sign_event' }, // Basic event signing
|
||||||
// but the TypeScript interface is ready for when it is
|
{ type: 'sign_event', kind: 0 }, // Profile metadata
|
||||||
const { pubkey, packageName } = await NDKAmberSigner.requestPublicKey();
|
{ type: 'sign_event', kind: 1 }, // Notes
|
||||||
|
{ type: 'sign_event', kind: 3 }, // Contacts
|
||||||
|
{ type: 'sign_event', kind: 4 }, // DMs
|
||||||
|
{ type: 'sign_event', kind: 6 }, // Reposts
|
||||||
|
{ type: 'sign_event', kind: 7 }, // Reactions
|
||||||
|
{ type: 'sign_event', kind: 9734 }, // Zaps
|
||||||
|
{ type: 'sign_event', kind: 1111 }, // Comments (NIP-22)
|
||||||
|
|
||||||
// Login with the external signer
|
// POWR-specific event kinds
|
||||||
const success = await loginWithExternalSigner(pubkey, packageName);
|
{ type: 'sign_event', kind: 1301 }, // Workout Record (1301)
|
||||||
|
{ type: 'sign_event', kind: 33401 }, // Exercise Template (33401)
|
||||||
|
{ type: 'sign_event', kind: 33402 }, // Workout Template (33402)
|
||||||
|
];
|
||||||
|
|
||||||
if (success) {
|
// Request public key from Amber
|
||||||
onClose();
|
const { pubkey, packageName } = await NDKAmberSigner.requestPublicKey(defaultPermissions);
|
||||||
} else {
|
|
||||||
setError('Failed to login with Amber');
|
// Login with the external signer
|
||||||
}
|
const success = await loginWithExternalSigner(pubkey, packageName);
|
||||||
} catch (requestError) {
|
|
||||||
// Since the native implementation is not available yet,
|
if (success) {
|
||||||
// we show a more user-friendly error message
|
onClose();
|
||||||
console.error('Amber requestPublicKey error:', requestError);
|
} else {
|
||||||
setError("Amber signing requires a native module implementation. The interface is ready but the native code needs to be completed.");
|
setError('Failed to login with Amber');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Amber login error:', err);
|
console.error('Amber login error:', err);
|
||||||
setError(err instanceof Error ? err.message : 'An unexpected error with Amber');
|
|
||||||
|
// Provide helpful error messages based on common issues
|
||||||
|
if (err instanceof Error) {
|
||||||
|
if (err.message.includes('Failed to get public key')) {
|
||||||
|
setError('Unable to get key from Amber. Please make sure Amber is installed and try again.');
|
||||||
|
} else if (err.message.includes('User cancelled')) {
|
||||||
|
setError('Login cancelled by user.');
|
||||||
|
} else {
|
||||||
|
setError(err.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setError('An unexpected error occurred with Amber.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -128,26 +148,27 @@ export default function NostrLoginSheet({ open, onClose }: NostrLoginSheetProps)
|
|||||||
<View className="space-y-4">
|
<View className="space-y-4">
|
||||||
{/* External signer option (Android only) */}
|
{/* External signer option (Android only) */}
|
||||||
{Platform.OS === 'android' && (
|
{Platform.OS === 'android' && (
|
||||||
<Button
|
<>
|
||||||
onPress={handleAmberLogin}
|
<Button
|
||||||
disabled={isLoading}
|
onPress={handleAmberLogin}
|
||||||
className="mb-3 py-3"
|
disabled={isLoading}
|
||||||
variant="outline"
|
className="mb-3 py-3"
|
||||||
style={{ borderColor: 'hsl(261, 90%, 66%)' }}
|
variant="outline"
|
||||||
>
|
style={{ borderColor: 'hsl(261 90% 66%)' }}
|
||||||
{isLoading ? (
|
>
|
||||||
<ActivityIndicator size="small" color="hsl(261, 90%, 66%)" />
|
{isLoading ? (
|
||||||
) : (
|
<ActivityIndicator size="small" color="hsl(261 90% 66%)" />
|
||||||
<View className="flex-row items-center">
|
) : (
|
||||||
<ExternalLink size={18} className="mr-2" color="hsl(261, 90%, 66%)" />
|
<View className="flex-row items-center">
|
||||||
<Text className="font-medium" style={{ color: 'hsl(261, 90%, 66%)' }}>Sign with Amber</Text>
|
<ExternalLink size={18} className="mr-2" color="hsl(261 90% 66%)" />
|
||||||
</View>
|
<Text className="font-medium" style={{ color: 'hsl(261 90% 66%)' }}>Sign with Amber</Text>
|
||||||
)}
|
</View>
|
||||||
</Button>
|
)}
|
||||||
|
</Button>
|
||||||
|
<Text className="text-sm text-muted-foreground mb-3">- or -</Text>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Text className="text-sm text-muted-foreground mb-3">- or -</Text>
|
|
||||||
|
|
||||||
<Text className="text-base">Enter your Nostr private key (nsec)</Text>
|
<Text className="text-base">Enter your Nostr private key (nsec)</Text>
|
||||||
<Input
|
<Input
|
||||||
placeholder="nsec1..."
|
placeholder="nsec1..."
|
||||||
@ -179,7 +200,7 @@ export default function NostrLoginSheet({ open, onClose }: NostrLoginSheetProps)
|
|||||||
onPress={handleLogin}
|
onPress={handleLogin}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="flex-1 py-3"
|
className="flex-1 py-3"
|
||||||
style={{ backgroundColor: 'hsl(261, 90%, 66%)' }}
|
style={{ backgroundColor: 'hsl(261 90% 66%)' }}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<ActivityIndicator size="small" color="#fff" />
|
<ActivityIndicator size="small" color="#fff" />
|
||||||
|
94
docs/technical/nostr/amber-integration-fixes.md
Normal file
94
docs/technical/nostr/amber-integration-fixes.md
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# Amber Signer Integration Fixes
|
||||||
|
|
||||||
|
## Issue Summary
|
||||||
|
|
||||||
|
The POWR app was experiencing issues when trying to integrate with the Amber external signer on Android. Specifically, the app was receiving "No data returned from Amber" errors when attempting to request a public key or sign events.
|
||||||
|
|
||||||
|
## Root Causes and Fixes
|
||||||
|
|
||||||
|
### 1. Improved Native Module Implementation
|
||||||
|
|
||||||
|
The primary issue was in the `AmberSignerModule.kt` implementation, which handles the communication between the React Native app and the Amber app:
|
||||||
|
|
||||||
|
- **Enhanced Intent Construction**: Modified how we create the intent to Amber to better follow NIP-55 protocol requirements.
|
||||||
|
- For `signEvent`, we now pass the event as an extra rather than in the URI itself to prevent potential payload size issues.
|
||||||
|
|
||||||
|
- **Implemented Better Response Handling**:
|
||||||
|
- Added logic to check both intent extras and URI parameters for responses
|
||||||
|
- Added validation to ensure signature data exists before resolving promises
|
||||||
|
- Improved error reporting with more specific messages
|
||||||
|
|
||||||
|
- **Added Extensive Logging**:
|
||||||
|
- Added detailed log statements throughout to help diagnose issues
|
||||||
|
- Log extras received from intent responses
|
||||||
|
- Log the entire response object before resolving
|
||||||
|
|
||||||
|
### 2. Fixed JavaScript Implementation
|
||||||
|
|
||||||
|
Several minor issues in the JavaScript files were also fixed:
|
||||||
|
|
||||||
|
- **ExternalSignerUtils.ts**:
|
||||||
|
- Added additional logging to track request/response flow
|
||||||
|
- Improved error handling with more descriptive messages
|
||||||
|
- Added checks for null/undefined responses
|
||||||
|
|
||||||
|
- **NostrLoginSheet.tsx**:
|
||||||
|
- Fixed syntax issues with missing commas in function parameters
|
||||||
|
- Added comprehensive Nostr event kind permissions including:
|
||||||
|
- Standard Nostr kinds: 0 (metadata), 1 (notes), 3 (contacts), 4 (DMs), 6 (reposts), 7 (reactions), 9734 (zaps)
|
||||||
|
- Comments kind 1111 (as defined in NIP-22)
|
||||||
|
- POWR-specific workout event kinds:
|
||||||
|
- Kind 1301: Workout Records - Stores completed workout sessions
|
||||||
|
- Kind 33401: Exercise Templates - Defines reusable exercise definitions with detailed form instructions
|
||||||
|
- Kind 33402: Workout Templates - Defines complete workout plans with associated exercises
|
||||||
|
|
||||||
|
### 3. POWR-Specific Nostr Event Kinds
|
||||||
|
|
||||||
|
Our app uses custom Nostr event kinds as defined in our exercise NIP (NIP-4e) proposal:
|
||||||
|
|
||||||
|
#### Exercise Template (kind: 33401)
|
||||||
|
These are parameterized replaceable events that define reusable exercise definitions with:
|
||||||
|
- Title, equipment type, and difficulty level
|
||||||
|
- Format parameters (weight, reps, RPE, set_type)
|
||||||
|
- Units for each parameter
|
||||||
|
- Optional media demonstrations
|
||||||
|
- Detailed form instructions in the content
|
||||||
|
|
||||||
|
#### Workout Template (kind: 33402)
|
||||||
|
These are parameterized replaceable events that define workout plans with:
|
||||||
|
- Title and workout type (strength, circuit, EMOM, AMRAP)
|
||||||
|
- Exercise references with prescribed parameters
|
||||||
|
- Optional rounds, duration, intervals, and rest times
|
||||||
|
- Workout instructions in the content
|
||||||
|
|
||||||
|
#### Workout Record (kind: 1301)
|
||||||
|
These are standard events that record completed workouts with:
|
||||||
|
- Start and end timestamps
|
||||||
|
- Exercises performed with actual values
|
||||||
|
- Completion status and rounds completed
|
||||||
|
- Optional reference to the template used
|
||||||
|
- Personal records achieved
|
||||||
|
- Notes about the workout experience
|
||||||
|
|
||||||
|
## Testing the Fix
|
||||||
|
|
||||||
|
To verify that the Amber integration is working correctly:
|
||||||
|
|
||||||
|
1. Make sure you have the Amber app installed on your Android device
|
||||||
|
2. In the POWR app, try to log in using the "Sign with Amber" button
|
||||||
|
3. Amber should launch and ask for permission to share your public key and sign the specified event kinds
|
||||||
|
4. After granting permission, you should be logged in successfully
|
||||||
|
|
||||||
|
If you encounter any issues, the extensive logging added should help identify the specific point of failure.
|
||||||
|
|
||||||
|
## Common Issues and Troubleshooting
|
||||||
|
|
||||||
|
- **Amber Not Launching**: Make sure the Amber app is installed and up to date. The app should be available at package name `com.greenart7c3.nostrsigner`.
|
||||||
|
|
||||||
|
- **Permission Denied**: Ensure the Android app has been granted necessary permissions.
|
||||||
|
|
||||||
|
- **Signature Missing**: If Amber returns data but the signature is missing, check the Amber app version and ensure it supports the NIP-55 protocol.
|
||||||
|
|
||||||
|
- **App Crash During Launch**: This could indicate an issue with the intent construction. Check the logs for details.
|
||||||
|
|
||||||
|
The improvements made to the error handling and logging should make it much easier to diagnose any remaining issues.
|
@ -1,4 +1,3 @@
|
|||||||
// lib/hooks/useNDK.ts
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useNDKStore } from '@/lib/stores/ndk';
|
import { useNDKStore } from '@/lib/stores/ndk';
|
||||||
import type { NDKUser, NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk-mobile';
|
import type { NDKUser, NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk-mobile';
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
// lib/signers/NDKAmberSigner.ts
|
|
||||||
import NDK, { type NDKSigner, type NDKUser, type NDKEncryptionScheme } from '@nostr-dev-kit/ndk-mobile';
|
import NDK, { type NDKSigner, type NDKUser, type NDKEncryptionScheme } from '@nostr-dev-kit/ndk-mobile';
|
||||||
import { Platform, Linking } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
import type { NostrEvent } from 'nostr-tools';
|
import type { NostrEvent } from 'nostr-tools';
|
||||||
import ExternalSignerUtils from '@/utils/ExternalSignerUtils';
|
import ExternalSignerUtils, { NIP55Permission } from '@/utils/ExternalSignerUtils';
|
||||||
|
import { nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NDK Signer implementation for Amber (NIP-55 compatible external signer)
|
* NDK Signer implementation for Amber (NIP-55 compatible external signer)
|
||||||
@ -10,7 +10,7 @@ import ExternalSignerUtils from '@/utils/ExternalSignerUtils';
|
|||||||
* This signer delegates signing operations to the Amber app on Android
|
* This signer delegates signing operations to the Amber app on Android
|
||||||
* through the use of Intent-based communication as defined in NIP-55.
|
* through the use of Intent-based communication as defined in NIP-55.
|
||||||
*
|
*
|
||||||
* Note: This is Android-specific and will need native module support.
|
* Note: This is Android-specific and requires the AmberSignerModule native module.
|
||||||
*/
|
*/
|
||||||
export class NDKAmberSigner implements NDKSigner {
|
export class NDKAmberSigner implements NDKSigner {
|
||||||
/**
|
/**
|
||||||
@ -21,7 +21,7 @@ export class NDKAmberSigner implements NDKSigner {
|
|||||||
/**
|
/**
|
||||||
* The package name of the Amber app
|
* The package name of the Amber app
|
||||||
*/
|
*/
|
||||||
private packageName: string | null = 'com.greenart7c3.nostrsigner';
|
private packageName: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this signer can sign events
|
* Whether this signer can sign events
|
||||||
@ -34,7 +34,7 @@ export class NDKAmberSigner implements NDKSigner {
|
|||||||
* @param pubkey The user's public key (hex)
|
* @param pubkey The user's public key (hex)
|
||||||
* @param packageName Optional Amber package name (default: com.greenart7c3.nostrsigner)
|
* @param packageName Optional Amber package name (default: com.greenart7c3.nostrsigner)
|
||||||
*/
|
*/
|
||||||
constructor(pubkey: string, packageName: string | null = 'com.greenart7c3.nostrsigner') {
|
constructor(pubkey: string, packageName: string = 'com.greenart7c3.nostrsigner') {
|
||||||
this.pubkey = pubkey;
|
this.pubkey = pubkey;
|
||||||
this.packageName = packageName;
|
this.packageName = packageName;
|
||||||
this.canSign = Platform.OS === 'android';
|
this.canSign = Platform.OS === 'android';
|
||||||
@ -75,8 +75,7 @@ export class NDKAmberSigner implements NDKSigner {
|
|||||||
/**
|
/**
|
||||||
* Sign an event using Amber
|
* Sign an event using Amber
|
||||||
*
|
*
|
||||||
* This will need to be implemented with native Android modules to handle
|
* Uses the native module to send an intent to Amber for signing.
|
||||||
* Intent-based communication with Amber.
|
|
||||||
*
|
*
|
||||||
* @param event The event to sign
|
* @param event The event to sign
|
||||||
* @returns The signature for the event
|
* @returns The signature for the event
|
||||||
@ -87,19 +86,23 @@ export class NDKAmberSigner implements NDKSigner {
|
|||||||
throw new Error('NDKAmberSigner is only available on Android');
|
throw new Error('NDKAmberSigner is only available on Android');
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a placeholder for the actual native implementation
|
try {
|
||||||
// In a full implementation, this would use the React Native bridge to call
|
// Get the npub representation of the hex pubkey
|
||||||
// native Android code that would handle the Intent-based communication with Amber
|
const npub = nip19.npubEncode(this.pubkey);
|
||||||
|
|
||||||
console.log('Amber signing event:', event);
|
// Use ExternalSignerUtils to sign the event
|
||||||
|
const response = await ExternalSignerUtils.signEvent(event, npub);
|
||||||
|
|
||||||
// Placeholder implementation - in a real implementation, we would:
|
if (!response.signature) {
|
||||||
// 1. Convert the event to JSON
|
throw new Error('No signature returned from Amber');
|
||||||
// 2. Create an Intent to send to Amber
|
}
|
||||||
// 3. Wait for the result from Amber
|
|
||||||
// 4. Extract the signature from the result
|
|
||||||
|
|
||||||
throw new Error('NDKAmberSigner.sign() not implemented');
|
return response.signature;
|
||||||
|
} catch (e: unknown) {
|
||||||
|
console.error('Error signing with Amber:', e);
|
||||||
|
const errorMessage = e instanceof Error ? e.message : 'Unknown error';
|
||||||
|
throw new Error(`Failed to sign event with Amber: ${errorMessage}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -129,24 +132,48 @@ export class NDKAmberSigner implements NDKSigner {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Static method to request public key from Amber
|
* Static method to request public key from Amber
|
||||||
* This needs to be implemented with native modules.
|
* Uses the ExternalSignerUtils to communicate with Amber.
|
||||||
*
|
*
|
||||||
* @returns Promise with public key and package name
|
* @param permissions Optional array of permissions to request
|
||||||
|
* @returns Promise with public key (hex) and package name
|
||||||
*/
|
*/
|
||||||
static async requestPublicKey(): Promise<{pubkey: string, packageName: string}> {
|
static async requestPublicKey(
|
||||||
|
permissions: NIP55Permission[] = []
|
||||||
|
): Promise<{pubkey: string, packageName: string}> {
|
||||||
if (Platform.OS !== 'android') {
|
if (Platform.OS !== 'android') {
|
||||||
throw new Error('NDKAmberSigner is only available on Android');
|
throw new Error('NDKAmberSigner is only available on Android');
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a placeholder for the actual native implementation
|
try {
|
||||||
// In a full implementation, this would launch an Intent to get the user's public key from Amber
|
// Request public key from Amber
|
||||||
|
console.log('[NDKAmberSigner] Requesting public key from Amber');
|
||||||
|
const result = await ExternalSignerUtils.requestPublicKey(permissions);
|
||||||
|
console.log('[NDKAmberSigner] Received result from ExternalSignerUtils:', result);
|
||||||
|
|
||||||
// Since this requires native code implementation, we'll throw an error
|
// Convert npub to hex if needed
|
||||||
// indicating that this functionality needs to be implemented with native modules
|
let pubkeyHex = result.pubkey;
|
||||||
throw new Error('NDKAmberSigner.requestPublicKey() requires native implementation');
|
if (pubkeyHex.startsWith('npub')) {
|
||||||
|
try {
|
||||||
|
// Decode the npub to get the hex pubkey
|
||||||
|
const decoded = nip19.decode(pubkeyHex);
|
||||||
|
if (decoded.type === 'npub') {
|
||||||
|
pubkeyHex = decoded.data as string;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error decoding npub:', e);
|
||||||
|
throw new Error('Invalid npub returned from Amber');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// When implemented, this would return:
|
return {
|
||||||
// return { pubkey: 'hex_pubkey_from_amber', packageName: 'com.greenart7c3.nostrsigner' };
|
pubkey: pubkeyHex,
|
||||||
|
packageName: result.packageName
|
||||||
|
};
|
||||||
|
} catch (e: unknown) {
|
||||||
|
console.error('Error requesting public key from Amber:', e);
|
||||||
|
const errorMessage = e instanceof Error ? e.message : 'Unknown error';
|
||||||
|
throw new Error(`Failed to get public key from Amber: ${errorMessage}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,40 +253,88 @@ const useWorkoutStoreBase = create<ExtendedWorkoutState & ExtendedWorkoutActions
|
|||||||
|
|
||||||
// Use NDK to publish
|
// Use NDK to publish
|
||||||
try {
|
try {
|
||||||
// Create a new event
|
try {
|
||||||
const event = new NDKEvent(ndk as any);
|
console.log('Starting workout event publish...');
|
||||||
|
|
||||||
// Set the properties
|
// Create a new event
|
||||||
event.kind = eventData.kind;
|
const event = new NDKEvent(ndk as any);
|
||||||
event.content = eventData.content;
|
|
||||||
event.tags = eventData.tags || [];
|
|
||||||
event.created_at = eventData.created_at;
|
|
||||||
|
|
||||||
// Sign and publish
|
// Set the properties
|
||||||
await event.sign();
|
event.kind = eventData.kind;
|
||||||
await event.publish();
|
event.content = eventData.content;
|
||||||
|
event.tags = eventData.tags || [];
|
||||||
|
event.created_at = eventData.created_at;
|
||||||
|
|
||||||
console.log('Successfully published workout event');
|
// Add timeout for signing to prevent hanging
|
||||||
|
const signPromise = event.sign();
|
||||||
|
const signTimeout = new Promise<void>((_, reject) => {
|
||||||
|
setTimeout(() => reject(new Error('Signing timeout after 15 seconds')), 15000);
|
||||||
|
});
|
||||||
|
|
||||||
// Handle social share if selected
|
try {
|
||||||
if (options.shareOnSocial && options.socialMessage) {
|
// Race the sign operation against a timeout
|
||||||
const socialEventData = NostrWorkoutService.createSocialShareEvent(
|
await Promise.race([signPromise, signTimeout]);
|
||||||
event.id,
|
console.log('Event signed successfully');
|
||||||
options.socialMessage
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create an NDK event for the social share
|
// Add timeout for publishing as well
|
||||||
const socialEvent = new NDKEvent(ndk as any);
|
const publishPromise = event.publish();
|
||||||
socialEvent.kind = socialEventData.kind;
|
const publishTimeout = new Promise<void>((_, reject) => {
|
||||||
socialEvent.content = socialEventData.content;
|
setTimeout(() => reject(new Error('Publishing timeout after 15 seconds')), 15000);
|
||||||
socialEvent.tags = socialEventData.tags || [];
|
});
|
||||||
socialEvent.created_at = socialEventData.created_at;
|
|
||||||
|
|
||||||
// Sign and publish
|
await Promise.race([publishPromise, publishTimeout]);
|
||||||
await socialEvent.sign();
|
console.log('Successfully published workout event');
|
||||||
await socialEvent.publish();
|
|
||||||
|
|
||||||
console.log('Successfully published social share');
|
// Handle social share if selected
|
||||||
|
if (options.shareOnSocial && options.socialMessage) {
|
||||||
|
try {
|
||||||
|
const socialEventData = NostrWorkoutService.createSocialShareEvent(
|
||||||
|
event.id,
|
||||||
|
options.socialMessage
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create an NDK event for the social share
|
||||||
|
const socialEvent = new NDKEvent(ndk as any);
|
||||||
|
socialEvent.kind = socialEventData.kind;
|
||||||
|
socialEvent.content = socialEventData.content;
|
||||||
|
socialEvent.tags = socialEventData.tags || [];
|
||||||
|
socialEvent.created_at = socialEventData.created_at;
|
||||||
|
|
||||||
|
// Sign with timeout
|
||||||
|
const socialSignPromise = socialEvent.sign();
|
||||||
|
const socialSignTimeout = new Promise<void>((_, reject) => {
|
||||||
|
setTimeout(() => reject(new Error('Social signing timeout after 15 seconds')), 15000);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.race([socialSignPromise, socialSignTimeout]);
|
||||||
|
|
||||||
|
// Publish with timeout
|
||||||
|
const socialPublishPromise = socialEvent.publish();
|
||||||
|
const socialPublishTimeout = new Promise<void>((_, reject) => {
|
||||||
|
setTimeout(() => reject(new Error('Social publishing timeout after 15 seconds')), 15000);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.race([socialPublishPromise, socialPublishTimeout]);
|
||||||
|
console.log('Successfully published social share');
|
||||||
|
} catch (socialError) {
|
||||||
|
console.error('Error publishing social share:', socialError);
|
||||||
|
// Continue with workout completion even if social sharing fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const signError = error as Error;
|
||||||
|
console.error('Error signing or publishing event:', signError);
|
||||||
|
|
||||||
|
// Specific handling for timeout errors to give user better feedback
|
||||||
|
if (signError.message?.includes('timeout')) {
|
||||||
|
console.warn('The signing operation timed out. This may be due to an issue with the external signer.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue with workout completion even though publishing failed
|
||||||
|
}
|
||||||
|
} catch (eventCreationError) {
|
||||||
|
console.error('Error creating event:', eventCreationError);
|
||||||
|
// Continue with workout completion, but log the error
|
||||||
}
|
}
|
||||||
} catch (publishError) {
|
} catch (publishError) {
|
||||||
console.error('Error publishing to Nostr:', publishError);
|
console.error('Error publishing to Nostr:', publishError);
|
||||||
|
@ -1,5 +1,23 @@
|
|||||||
// utils/ExternalSignerUtils.ts
|
import { Platform, NativeModules } from 'react-native';
|
||||||
import { Platform } from 'react-native';
|
|
||||||
|
// Interface for NIP-55 permission
|
||||||
|
export interface NIP55Permission {
|
||||||
|
type: string;
|
||||||
|
kind?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface for the response from the native module
|
||||||
|
export interface AmberResponse {
|
||||||
|
signature?: string;
|
||||||
|
id?: string;
|
||||||
|
event?: string;
|
||||||
|
packageName?: string;
|
||||||
|
results?: Array<{
|
||||||
|
signature?: string;
|
||||||
|
id?: string;
|
||||||
|
packageName?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility functions for interacting with external Nostr signers (Android only)
|
* Utility functions for interacting with external Nostr signers (Android only)
|
||||||
@ -9,71 +27,117 @@ class ExternalSignerUtils {
|
|||||||
/**
|
/**
|
||||||
* Check if an external signer is installed (Amber)
|
* Check if an external signer is installed (Amber)
|
||||||
*
|
*
|
||||||
* Note: This needs to be implemented in native code since it requires
|
* Uses the native Android module to check if Amber is installed.
|
||||||
* access to Android's PackageManager to query for activities that can
|
|
||||||
* handle the nostrsigner: scheme. This is a placeholder for the TypeScript
|
|
||||||
* interface.
|
|
||||||
*
|
*
|
||||||
* @returns {Promise<boolean>} True if an external signer is available
|
* @returns {Promise<boolean>} True if an external signer is available
|
||||||
*/
|
*/
|
||||||
static isExternalSignerInstalled(): Promise<boolean> {
|
static async isExternalSignerInstalled(): Promise<boolean> {
|
||||||
// This would need to be implemented with native modules
|
|
||||||
// For now, we'll return false if not on Android
|
|
||||||
if (Platform.OS !== 'android') {
|
if (Platform.OS !== 'android') {
|
||||||
return Promise.resolve(false);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add actual implementation that calls native code:
|
try {
|
||||||
// In native Android code:
|
const { AmberSignerModule } = NativeModules;
|
||||||
// val intent = Intent().apply {
|
if (AmberSignerModule) {
|
||||||
// action = Intent.ACTION_VIEW
|
return await AmberSignerModule.isExternalSignerInstalled();
|
||||||
// data = Uri.parse("nostrsigner:")
|
}
|
||||||
// }
|
} catch (e) {
|
||||||
// val infos = context.packageManager.queryIntentActivities(intent, 0)
|
console.error('Error checking for external signer:', e);
|
||||||
// return infos.size > 0
|
}
|
||||||
|
|
||||||
// Placeholder implementation - this should be replaced with actual native code check
|
return false;
|
||||||
return Promise.resolve(false);
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request public key from external signer
|
||||||
|
*
|
||||||
|
* @param {Array<NIP55Permission>} permissions Default permissions to request
|
||||||
|
* @returns {Promise<{pubkey: string, packageName: string}>} Public key and package name of the signer
|
||||||
|
*/
|
||||||
|
static async requestPublicKey(
|
||||||
|
permissions: Array<NIP55Permission> = []
|
||||||
|
): Promise<{pubkey: string, packageName: string}> {
|
||||||
|
if (Platform.OS !== 'android') {
|
||||||
|
throw new Error('External signers are only supported on Android');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { AmberSignerModule } = NativeModules;
|
||||||
|
if (!AmberSignerModule) {
|
||||||
|
throw new Error('AmberSignerModule not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[ExternalSignerUtils] Calling AmberSignerModule.requestPublicKey');
|
||||||
|
const response = await AmberSignerModule.requestPublicKey(permissions);
|
||||||
|
console.log('[ExternalSignerUtils] Response from AmberSignerModule:', response);
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
throw new Error('No response received from Amber');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.signature) {
|
||||||
|
throw new Error(`Invalid response from Amber: ${JSON.stringify(response)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pubkey: response.signature, // Amber returns npub in the signature field
|
||||||
|
packageName: response.packageName || 'com.greenart7c3.nostrsigner'
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error requesting public key:', e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign an event using external signer
|
||||||
|
*
|
||||||
|
* @param {Object} event The event to sign
|
||||||
|
* @param {string} currentUserPubkey The current user's public key
|
||||||
|
* @param {string} eventId Optional ID for tracking the event
|
||||||
|
* @returns {Promise<AmberResponse>} The response from Amber including signature
|
||||||
|
*/
|
||||||
|
static async signEvent(
|
||||||
|
event: any,
|
||||||
|
currentUserPubkey: string,
|
||||||
|
eventId?: string
|
||||||
|
): Promise<AmberResponse> {
|
||||||
|
if (Platform.OS !== 'android') {
|
||||||
|
throw new Error('External signers are only supported on Android');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { AmberSignerModule } = NativeModules;
|
||||||
|
if (!AmberSignerModule) {
|
||||||
|
throw new Error('AmberSignerModule not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventJson = JSON.stringify(event);
|
||||||
|
console.log('[ExternalSignerUtils] Calling AmberSignerModule.signEvent');
|
||||||
|
const response = await AmberSignerModule.signEvent(
|
||||||
|
eventJson,
|
||||||
|
currentUserPubkey,
|
||||||
|
eventId || event.id || `event-${Date.now()}`
|
||||||
|
);
|
||||||
|
console.log('[ExternalSignerUtils] Response from AmberSignerModule.signEvent:', response);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error signing event:', e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format permissions for external signer requests
|
* Format permissions for external signer requests
|
||||||
*
|
*
|
||||||
* @param {Array<{type: string, kind?: number}>} permissions The permissions to request
|
* @param {Array<NIP55Permission>} permissions The permissions to request
|
||||||
* @returns {string} JSON string of permissions
|
* @returns {string} JSON string of permissions
|
||||||
*/
|
*/
|
||||||
static formatPermissions(permissions: Array<{type: string, kind?: number}>): string {
|
static formatPermissions(permissions: Array<NIP55Permission>): string {
|
||||||
return JSON.stringify(permissions);
|
return JSON.stringify(permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create intent parameters for getting public key from external signer
|
|
||||||
*
|
|
||||||
* @param {Array<{type: string, kind?: number}>} permissions Default permissions to request
|
|
||||||
* @returns {Object} Parameters for the intent
|
|
||||||
*/
|
|
||||||
static createGetPublicKeyParams(permissions: Array<{type: string, kind?: number}> = []): any {
|
|
||||||
return {
|
|
||||||
type: 'get_public_key',
|
|
||||||
permissions: this.formatPermissions(permissions)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create intent parameters for signing an event
|
|
||||||
*
|
|
||||||
* @param {Object} event The event to sign
|
|
||||||
* @param {string} currentUserPubkey The current user's public key
|
|
||||||
* @returns {Object} Parameters for the intent
|
|
||||||
*/
|
|
||||||
static createSignEventParams(event: any, currentUserPubkey: string): any {
|
|
||||||
return {
|
|
||||||
type: 'sign_event',
|
|
||||||
id: event.id || `event-${Date.now()}`,
|
|
||||||
current_user: currentUserPubkey
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if we're running on Android
|
* Check if we're running on Android
|
||||||
*
|
*
|
||||||
|
Loading…
x
Reference in New Issue
Block a user