From 77df638340077cab57213ea9bf47f9d18f877358 Mon Sep 17 00:00:00 2001 From: koalasat Date: Fri, 25 Jul 2025 17:33:19 +0200 Subject: [PATCH] Notifications --- android/app/build.gradle.kts | 10 +- .../main/java/com/robosats/MainActivity.kt | 32 +++++ .../main/java/com/robosats/WebAppInterface.kt | 8 +- .../java/com/robosats/models/NostrClient.kt | 122 ++++++++++++++---- .../robosats/services/NotificationsService.kt | 110 +++++++++++++++- android/gradle/libs.versions.toml | 2 + frontend/src/basic/Routes.tsx | 19 +-- .../components/HostAlert/SelfhostedAlert.tsx | 2 +- .../src/components/HostAlert/UnsafeAlert.tsx | 2 +- .../TradeBox/EncryptedChat/index.tsx | 2 +- frontend/src/contexts/AppContext.tsx | 4 +- frontend/src/models/Coordinator.model.ts | 4 +- frontend/src/models/Federation.model.ts | 6 + frontend/src/models/Garage.model.ts | 6 +- frontend/src/services/Android/index.ts | 15 ++- frontend/src/services/RoboPool/index.ts | 6 +- frontend/static/federation.json | 46 +++++++ 17 files changed, 336 insertions(+), 60 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index a71213e2..647e650d 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -71,10 +71,14 @@ dependencies { implementation(libs.material) implementation(libs.okhttp) implementation(libs.kmp.tor) - implementation(libs.quartz) - implementation(libs.ammolite) + implementation(libs.quartz) { + exclude("net.java.dev.jna") + } + implementation(libs.ammolite) { + exclude("net.java.dev.jna") + } + implementation(libs.jna) { artifact { type = "aar" } } implementation(libs.security.crypto.ktx) - // Add the KMP Tor binary dependency (contains the native .so files) implementation(libs.kmp.tor.binary) implementation(libs.androidx.activity) implementation(libs.androidx.constraintlayout) diff --git a/android/app/src/main/java/com/robosats/MainActivity.kt b/android/app/src/main/java/com/robosats/MainActivity.kt index c90825c2..d3365e7f 100644 --- a/android/app/src/main/java/com/robosats/MainActivity.kt +++ b/android/app/src/main/java/com/robosats/MainActivity.kt @@ -32,6 +32,8 @@ import com.robosats.models.EncryptedStorage import com.robosats.services.NotificationsService import com.robosats.tor.TorKmp import com.robosats.tor.TorKmpManager +import com.robosats.tor.TorKmpManager.getTorKmpObject +import com.vitorpamplona.ammolite.service.HttpClientManager class MainActivity : AppCompatActivity() { private val requestCodePostNotifications: Int = 1 @@ -39,6 +41,7 @@ class MainActivity : AppCompatActivity() { private lateinit var torKmp: TorKmp private lateinit var loadingContainer: ConstraintLayout private lateinit var statusTextView: TextView + private lateinit var intentData: String override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -74,8 +77,27 @@ class MainActivity : AppCompatActivity() { requestCodePostNotifications, ) } + + val intent = intent + if (intent != null) { + val orderId = intent.getStringExtra("order_id") + if (orderId?.isNotEmpty() == true) { + intentData = orderId + } + } } + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + intent.let { + val orderId = intent.getStringExtra("order_id") + if (orderId?.isNotEmpty() == true) { + intentData = orderId + } + } + } + + /** * Initialize Notifications service */ @@ -158,6 +180,8 @@ class MainActivity : AppCompatActivity() { runOnUiThread { updateStatus("Tor connected successfully. Setting up secure browser...") + HttpClientManager.setDefaultProxy(getTorKmpObject().proxy) + // Now that Tor is connected, set up the WebView setupWebView() } @@ -296,6 +320,14 @@ class MainActivity : AppCompatActivity() { webView.loadUrl("file:///android_asset/index.html") initializeNotifications() + + webView.post { + try { + webView.evaluateJavascript("javascript:window.AndroidDataRobosats = { navigateToPage: '$intentData' }", null) + } catch (e: Exception) { + Log.e("NavigateToPage", "Error evaluating JavaScript: $e") + } + } } } catch (e: Exception) { Log.e("WebViewSetup", "Security error in WebView setup: ${e.message}", e) diff --git a/android/app/src/main/java/com/robosats/WebAppInterface.kt b/android/app/src/main/java/com/robosats/WebAppInterface.kt index 3cae7c49..37452cf7 100644 --- a/android/app/src/main/java/com/robosats/WebAppInterface.kt +++ b/android/app/src/main/java/com/robosats/WebAppInterface.kt @@ -7,6 +7,7 @@ import android.webkit.JavascriptInterface import android.webkit.WebView import android.widget.Toast import com.robosats.models.EncryptedStorage +import com.robosats.models.NostrClient import com.robosats.tor.TorKmpManager.getTorKmpObject import okhttp3.Call import okhttp3.Callback @@ -166,11 +167,10 @@ class WebAppInterface(private val context: Context, private val webView: WebView } try { - Log.d(TAG, "WebSocket opening: " + path) + Log.d(TAG, "WebSocket opening: $path") val client: OkHttpClient = Builder() .connectTimeout(60, TimeUnit.SECONDS) // Set connection timeout .readTimeout(30, TimeUnit.SECONDS) // Set read timeout - .proxy(getTorKmpObject().proxy) .build() @@ -236,7 +236,7 @@ class WebAppInterface(private val context: Context, private val webView: WebView return } - val websocket = webSockets.get(path) + val websocket = webSockets[path] if (websocket != null) { websocket.send(message) resolvePromise(uuid, "true") @@ -356,6 +356,8 @@ class WebAppInterface(private val context: Context, private val webView: WebView EncryptedStorage.setEncryptedStorage(sanitizedKey, sanitizedValue) + if (key == "federation_relays") NostrClient.refresh() + // Safely encode and return the result resolvePromise(uuid, key) } diff --git a/android/app/src/main/java/com/robosats/models/NostrClient.kt b/android/app/src/main/java/com/robosats/models/NostrClient.kt index 78107bbb..1e6e6dea 100644 --- a/android/app/src/main/java/com/robosats/models/NostrClient.kt +++ b/android/app/src/main/java/com/robosats/models/NostrClient.kt @@ -7,6 +7,9 @@ import com.vitorpamplona.ammolite.relays.Relay import com.vitorpamplona.ammolite.relays.RelayPool import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.crypto.KeyPair +import com.vitorpamplona.quartz.encoders.Hex +import org.json.JSONArray import org.json.JSONObject object NostrClient { @@ -25,6 +28,15 @@ object NostrClient { subscribeToInbox() } + fun refresh() { + val federationRelays = EncryptedStorage.getEncryptedStorage("federation_relays") + val relayPool = RelayPool.getAll().map { it.url } + if (federationRelays.toSet() != relayPool.toSet()) { + stop() + start() + } + } + fun checkRelaysHealth() { if (RelayPool.getAll().isEmpty()) { stop() @@ -41,45 +53,101 @@ object NostrClient { } } - private fun connectRelays() { + fun garagePubKeys(): List { val garageString = EncryptedStorage.getEncryptedStorage("garage_slots") + var authors = emptyList() - val garage = JSONObject(garageString) + if (garageString.isNotEmpty()) { + val garage = JSONObject(garageString) - val relays = emptyList() + val keys = garage.keys() + while (keys.hasNext()) { + val key = keys.next() + val slot = garage.getJSONObject(key) // Get the value associated with the key + val hexPubKey = slot.getString("nostrPubKey") + if (hexPubKey.isNotEmpty()) { + authors = authors.plus(hexPubKey) + } + } + } - relays.forEach { - Client.sendFilterOnlyIfDisconnected() - if (RelayPool.getRelays(it).isEmpty()) { - RelayPool.addRelay( - Relay( - it, - read = true, - write = false, - forceProxy = false, - activeTypes = COMMON_FEED_TYPES, - ), - ) + return authors + } + + fun getRobotKeyPair(hexPubKey: String): KeyPair { + val garageString = EncryptedStorage.getEncryptedStorage("garage_slots") + var privKey = "" + var pubKey = "" + + if (garageString.isNotEmpty()) { + val garage = JSONObject(garageString) + + val keys = garage.keys() + while (keys.hasNext() && privKey == "") { + val key = keys.next() + val slot = garage.getJSONObject(key) + val slotPubKey = slot.getString("nostrPubKey") + if (slotPubKey == hexPubKey) { + pubKey = slotPubKey + val nostrSecKeyJson = slot.getJSONObject("nostrSecKey") + val byteArray = ByteArray(nostrSecKeyJson.length()) { index -> + nostrSecKeyJson.getInt(index.toString()).toByte() + } + privKey = byteArray.joinToString("") { byte -> "%02x".format(byte) } + } + } + } + + return KeyPair( + Hex.decode(privKey), + Hex.decode(pubKey), + ) + } + + private fun connectRelays() { + val federationRelays = EncryptedStorage.getEncryptedStorage("federation_relays") + + if (federationRelays.isNotEmpty()) { + val relaysUrls = JSONArray(federationRelays) + + for (i in 0 until relaysUrls.length()) { + val url = relaysUrls.getString(i) + Client.sendFilterOnlyIfDisconnected() + if (RelayPool.getRelays(url).isEmpty()) { + RelayPool.addRelay( + Relay( + url, + read = true, + write = false, + forceProxy = true, + activeTypes = COMMON_FEED_TYPES, + ), + ) + } } } } private fun subscribeToInbox() { - val authors = emptyList() + val garageString = EncryptedStorage.getEncryptedStorage("garage_slots") - if (authors.isNotEmpty()) { - Client.sendFilter( - subscriptionNotificationId, - listOf( - TypedFilter( - types = COMMON_FEED_TYPES, - filter = SincePerRelayFilter( - kinds = listOf(1, 4, 6, 7, 9735), - tags = mapOf("p" to authors), + if (garageString.isNotEmpty()) { + val authors = garagePubKeys() + + if (authors.isNotEmpty()) { + Client.sendFilter( + subscriptionNotificationId, + listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = SincePerRelayFilter( + kinds = listOf(1059), + tags = mapOf("p" to authors), + ), ), ), - ), - ) + ) + } } } } diff --git a/android/app/src/main/java/com/robosats/services/NotificationsService.kt b/android/app/src/main/java/com/robosats/services/NotificationsService.kt index 9324e865..837c685f 100644 --- a/android/app/src/main/java/com/robosats/services/NotificationsService.kt +++ b/android/app/src/main/java/com/robosats/services/NotificationsService.kt @@ -2,12 +2,20 @@ package com.robosats.services import android.Manifest import android.app.Notification import android.app.NotificationManager +import android.app.PendingIntent import android.app.Service import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Path import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities +import android.net.Uri import android.os.IBinder +import android.util.Base64 import android.util.Log import androidx.annotation.RequiresPermission import androidx.core.app.NotificationChannelCompat @@ -16,18 +24,29 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.robosats.Connectivity import com.robosats.R +import com.robosats.RoboIdentities +import com.robosats.models.EncryptedStorage import com.robosats.models.NostrClient +import com.robosats.models.NostrClient.garagePubKeys +import com.robosats.models.NostrClient.getRobotKeyPair import com.vitorpamplona.ammolite.relays.Client import com.vitorpamplona.ammolite.relays.Relay +import com.vitorpamplona.quartz.events.ChatMessageEvent import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.GiftWrapEvent +import com.vitorpamplona.quartz.events.SealedGossipEvent +import com.vitorpamplona.quartz.signers.NostrSignerInternal import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.json.JSONObject import java.util.Timer import java.util.TimerTask import java.util.concurrent.ConcurrentHashMap +import androidx.core.graphics.createBitmap +import com.robosats.MainActivity class NotificationsService : Service() { private var channelRelaysId = "RelaysConnections" @@ -35,6 +54,7 @@ class NotificationsService : Service() { private lateinit var notificationGroup: NotificationChannelGroupCompat + private val roboIdentities = RoboIdentities() private val timer = Timer() private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val processedEvents = ConcurrentHashMap() @@ -47,12 +67,24 @@ class NotificationsService : Service() { relay: Relay, afterEOSE: Boolean, ) { - if (processedEvents.putIfAbsent(event.id, true) == null) { + if (event is GiftWrapEvent && processedEvents.putIfAbsent(event.id, true) == null) { Log.d("RobosatsNotifications", "Relay Event: ${relay.url} - $subscriptionId - ${event.toJson()}") - val notify = true + val firstTaggedUser = event.firstTaggedUser() + val authors = garagePubKeys() - if (notify) { - Log.d("RobosatsNotifications", "Relay Event: ${relay.url} - $subscriptionId - Broadcast") + if (firstTaggedUser?.isNotEmpty() == true && authors.contains(firstTaggedUser)) { + Log.d("RobosatsNotifications", "Relay Event: ${relay.url} - $subscriptionId") + + var nostrSigner = NostrSignerInternal(getRobotKeyPair(firstTaggedUser)) + event.unwrap(nostrSigner) { gift -> + if (gift is SealedGossipEvent) { + gift.unseal(nostrSigner) { rumor -> + if (rumor is ChatMessageEvent) { + displayOrderNotification(rumor, firstTaggedUser) + } + } + } + } } } } @@ -211,4 +243,74 @@ class NotificationsService : Service() { notificationManager.notify(1, build) return build } + + private fun displayOrderNotification(event: ChatMessageEvent, hexPubKey: String) { + val notificationManager = + getSystemService(NOTIFICATION_SERVICE) as NotificationManager + + val orderId = event.firstTag("order_id") + + val intent = Intent(applicationContext, MainActivity::class.java).apply { + putExtra("order_id", orderId) + } + val pendingIntent = PendingIntent.getActivity( + applicationContext, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE + ) + + val garageString = EncryptedStorage.getEncryptedStorage("garage_slots") + var bitmap: Bitmap? = null + + if (garageString.isNotEmpty()) { + val garage = JSONObject(garageString) + val keys = garage.keys() + while (keys.hasNext() && bitmap == null) { + val key = keys.next() + val slot = garage.getJSONObject(key) + val slotHexPubKey = slot.getString("nostrPubKey") + if (hexPubKey.isNotEmpty() && hexPubKey == slotHexPubKey) { + val hashId = slot.getString("hashId") + if (hashId.isNotEmpty()) { + val base64Image = roboIdentities.generateRobohash("$hashId;80") + val imageBytes = Base64.decode(base64Image, Base64.DEFAULT) + val rawBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + bitmap = getRoundedBitmap(rawBitmap) + } + } + } + } + + val builder: NotificationCompat.Builder = + NotificationCompat.Builder( + applicationContext, + channelNotificationsId, + ) + .setContentTitle(orderId?.replace("/", "#")?.replaceFirstChar { it.uppercase() }) + .setContentText(event.content) + .setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(bitmap) + .setContentIntent(pendingIntent) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + + notificationManager.notify(event.id.hashCode(), builder.build()) + } + + private fun getRoundedBitmap(bitmap: Bitmap): Bitmap { + val output = createBitmap(bitmap.width, bitmap.height) + val canvas = Canvas(output) + val paint = Paint() + val path = Path() + + // Create a rounded rectangle path + path.addRoundRect(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat(), + bitmap.width / 2f, bitmap.height / 2f, Path.Direction.CW) + + canvas.clipPath(path) + canvas.drawBitmap(bitmap, 0f, 0f, paint) + + return output + } } diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index eb7c778c..00118893 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -12,6 +12,7 @@ activity = "1.10.1" constraintlayout = "2.2.1" kmpTor= "4.8.10-0-1.4.5" kmpTorBinary= "4.8.10-0" +jna = "5.14.0" okhttp = "5.0.0-alpha.14" quartz = "0.92.7" @@ -30,6 +31,7 @@ okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhtt quartz = { module = "com.github.vitorpamplona.amethyst:quartz", version.ref = "quartz" } ammolite = { module = "com.github.vitorpamplona.amethyst:ammolite", version.ref = "quartz" } security-crypto-ktx = { module = "androidx.security:security-crypto-ktx", version.ref = "securityCryptoKtx" } +jna = { module = "net.java.dev.jna:jna", version.ref = "jna" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/frontend/src/basic/Routes.tsx b/frontend/src/basic/Routes.tsx index 11cbf13a..56e4bb13 100644 --- a/frontend/src/basic/Routes.tsx +++ b/frontend/src/basic/Routes.tsx @@ -12,18 +12,19 @@ const Routes: React.FC = () => { const { page, navigateToPage } = useContext(AppContext); useEffect(() => { - window.addEventListener('navigateToPage', (event) => { - const orderId: string = event?.order_id; - const coordinator: string = event?.coordinator; + if (window.AndroidDataRobosats && garage.currentSlot) { + const orderPath = window.AndroidDataRobosats.navigateToPage ?? ''; + const [coordinator, orderId] = orderPath.replace('#', '/').split('/'); + window.AndroidDataRobosats = undefined; + if (orderId && coordinator) { const slot = garage.getSlotByOrder(coordinator, parseInt(orderId, 10)); - if (slot?.token) { - garage.setCurrentSlot(slot?.token); - navigateToPage(`order/${coordinator}/${orderId}`, navigate); - } + if (slot?.token) garage.setCurrentSlot(slot.token); + + navigateToPage(`order/${coordinator}/${orderId}`, navigate); } - }); - }, []); + } + }, [garage.currentSlot]); useEffect(() => { // change tab (page) into the current route diff --git a/frontend/src/components/HostAlert/SelfhostedAlert.tsx b/frontend/src/components/HostAlert/SelfhostedAlert.tsx index fbd268bf..ff35bee3 100644 --- a/frontend/src/components/HostAlert/SelfhostedAlert.tsx +++ b/frontend/src/components/HostAlert/SelfhostedAlert.tsx @@ -9,7 +9,7 @@ const SelfhostedAlert = (): React.JSX.Element => { useEffect(() => { systemClient.getItem('selfhosted-alert').then((result) => { - if (result) setShow(true); + if (!result) setShow(true); }); }, []); diff --git a/frontend/src/components/HostAlert/UnsafeAlert.tsx b/frontend/src/components/HostAlert/UnsafeAlert.tsx index 7b2f0f16..9b9facd7 100644 --- a/frontend/src/components/HostAlert/UnsafeAlert.tsx +++ b/frontend/src/components/HostAlert/UnsafeAlert.tsx @@ -42,7 +42,7 @@ const UnsafeAlert = (): React.JSX.Element => { useEffect(() => { systemClient.getItem('unsafe-alert').then((result) => { - if (result) setShow(true); + if (!result) setShow(true); }); }, []); diff --git a/frontend/src/components/TradeBox/EncryptedChat/index.tsx b/frontend/src/components/TradeBox/EncryptedChat/index.tsx index 4f929c54..6cad9a78 100644 --- a/frontend/src/components/TradeBox/EncryptedChat/index.tsx +++ b/frontend/src/components/TradeBox/EncryptedChat/index.tsx @@ -81,7 +81,7 @@ const EncryptedChat: React.FC = ({ try { const recipient = { publicKey, - relayUrl: coordinator.getRelayUrl(coordinator.url), + relayUrl: coordinator.getRelayUrl(), }; const wrappedEvent = nip17.wrapEvent(slot?.nostrSecKey, recipient, content); diff --git a/frontend/src/contexts/AppContext.tsx b/frontend/src/contexts/AppContext.tsx index 8bd3746f..53e8ec42 100644 --- a/frontend/src/contexts/AppContext.tsx +++ b/frontend/src/contexts/AppContext.tsx @@ -137,11 +137,11 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): React initialAppContext.acknowledgedWarning, ); - const navigateToPage: (newPage: Page, navigate: NavigateFunction) => void = ( + const navigateToPage: (newPage: Page | string, navigate: NavigateFunction) => void = ( newPage, navigate, ) => { - const pathPage: Page | string = newPage.split('/')[0]; + const pathPage: Page | string = newPage.replace('#', '/').split('/')[0]; if (isPage(pathPage)) { setPage(pathPage); navigate(`/${newPage}`); diff --git a/frontend/src/models/Coordinator.model.ts b/frontend/src/models/Coordinator.model.ts index 5c7dd89f..29e87079 100644 --- a/frontend/src/models/Coordinator.model.ts +++ b/frontend/src/models/Coordinator.model.ts @@ -281,8 +281,8 @@ export class Coordinator { this.book = {}; }; - getRelayUrl = (hostUrl: string): string => { - const protocol = hostUrl.includes('https') ? 'wss://' : 'ws://'; + getRelayUrl = (): string => { + const protocol = this.url.includes('https') ? 'wss://' : 'ws://'; return this.url.replace(/^https?:\/\//, protocol) + '/relay/'; }; } diff --git a/frontend/src/models/Federation.model.ts b/frontend/src/models/Federation.model.ts index 8561ea14..aeedd403 100644 --- a/frontend/src/models/Federation.model.ts +++ b/frontend/src/models/Federation.model.ts @@ -13,6 +13,7 @@ import { coordinatorDefaultValues } from './Coordinator.model'; import { updateExchangeInfo } from './Exchange.model'; import eventToPublicOrder from '../utils/nostr'; import RoboPool from '../services/RoboPool'; +import { systemClient } from '../services/System'; type FederationHooks = 'onFederationUpdate'; @@ -61,6 +62,11 @@ export class Federation { if (tesnetHost) this.network = 'testnet'; this.connection = null; this.roboPool = new RoboPool(settings); + + if (settings.client === 'mobile') { + const federationUrls = Object.values(this.coordinators).map((c) => c.getRelayUrl()); + systemClient.setItem('federation_relays', JSON.stringify(federationUrls)); + } } private coordinators: Record; diff --git a/frontend/src/models/Garage.model.ts b/frontend/src/models/Garage.model.ts index c3e35a5e..62462dfd 100644 --- a/frontend/src/models/Garage.model.ts +++ b/frontend/src/models/Garage.model.ts @@ -42,12 +42,14 @@ class Garage { save = (): void => { systemClient.setItem('garage_slots', JSON.stringify(this.slots)); + if (this.currentSlot) systemClient.setItem('garage_current_slot', this.currentSlot); }; delete = (): void => { this.slots = {}; this.currentSlot = null; systemClient.deleteItem('garage_slots'); + systemClient.deleteItem('garage_current_slot'); this.triggerHook('onSlotUpdate'); }; @@ -73,9 +75,11 @@ class Garage { ); this.slots[rawSlot.token].updateSlotFromOrder(new Order(rawSlot.lastOrder)); this.slots[rawSlot.token].updateSlotFromOrder(new Order(rawSlot.activeOrder)); - this.currentSlot = rawSlot?.token; } }); + + this.currentSlot = + (await systemClient.getItem('garage_current_slot')) ?? Object.keys(rawSlots)[0]; console.log('Robot Garage was loaded from local storage'); this.triggerHook('onSlotUpdate'); } diff --git a/frontend/src/services/Android/index.ts b/frontend/src/services/Android/index.ts index 6b4ea82a..9b3678cf 100644 --- a/frontend/src/services/Android/index.ts +++ b/frontend/src/services/Android/index.ts @@ -1,11 +1,16 @@ declare global { interface Window { + AndroidDataRobosats?: AndroidDataRobosats; AndroidAppRobosats?: AndroidAppRobosats; AndroidRobosats?: AndroidRobosats; RobosatsSettings: 'web-basic' | 'web-pro' | 'selfhosted-basic' | 'selfhosted-pro'; } } +interface AndroidDataRobosats { + navigateToPage: string; +} + interface AndroidAppRobosats { getEncryptedStorage: (uuid: string, key: string) => void; setEncryptedStorage: (uuid: string, key: string, value: string) => void; @@ -38,6 +43,10 @@ class AndroidRobosats { } > = {}; + public navigateToPage: (orderPath: string) => void = (orderPath) => { + console.log('orderPath', orderPath); + }; + public storePromise: ( uuid: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -86,15 +95,15 @@ class AndroidRobosats { }; public onWSMessage: (path: string, message: string) => void = (path, message) => { - this.WSConnections[path](message); + this.WSConnections[path]?.(message); }; public onWsError: (path: string) => void = (path) => { - this.WSError[path](); + this.WSError[path]?.(); }; public onWsClose: (path: string) => void = (path) => { - this.WSClose[path](); + this.WSClose[path]?.(); }; } diff --git a/frontend/src/services/RoboPool/index.ts b/frontend/src/services/RoboPool/index.ts index 69ccc3f7..d4530b65 100644 --- a/frontend/src/services/RoboPool/index.ts +++ b/frontend/src/services/RoboPool/index.ts @@ -25,9 +25,9 @@ class RoboPool { updateRelays = (hostUrl: string, coordinators: Coordinator[]) => { this.close(); this.relays = []; - const federationRelays = coordinators.map((coord) => coord.getRelayUrl(hostUrl)); - // const hostRelay = federationRelays.find((relay) => relay.includes(hostUrl)); - const hostRelay = 'ws://45gzfolhp3dcfv6w7a4p2iwekvurdjcf4p2onhnmvyhauwxfsx7kguad.onion/relay/'; + const federationRelays = coordinators.map((coord) => coord.getRelayUrl()); + const hostRelay = federationRelays.find((relay) => relay.includes(hostUrl)); + if (hostRelay) this.relays.push(hostRelay); while (this.relays.length < 3) { diff --git a/frontend/static/federation.json b/frontend/static/federation.json index 88452858..bd348acd 100644 --- a/frontend/static/federation.json +++ b/frontend/static/federation.json @@ -279,5 +279,51 @@ "mainnetNodesPubkeys": ["028059662c65626f3002290dd3151186bc1952726c03f56012406f3ceadb81cacc"], "testnetNodesPubkeys": [], "federated": true + }, + "satstralia": { + "longAlias": "Satstralia", + "shortAlias": "satstralia", + "identifier": "satstralia", + "description": "A Decentralized, deflationary currency enthusiast. Hoping to add liquidity to a non-KYC means for purchasing and selling bitcoin. Robosats P2P solves many issues with the modern day fiat system, and FreedomSats is happy to aid in its decentralization. The focus of FreedomSats will be to ensure data privacy and uptime-consistency.", + "motto": "Liberty’s Network Above", + "color": "#5d2365", + "established": "2025-06-30", + "nostrHexPubkey": "116bf3493edd2136f5e0c6a11b6c24d0eb34bbd5bf8ede965a9548726de710c7", + "contact": { + "email": "FreedomSatoshis@proton.me", + "telegram": "@freedomsats", + "simplex": "https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2Fh--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw%3D%40smp15.simplex.im%2FLjWY29_NGy7vujmzRCmnJocepWWMAk2D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAWRfXNPT2xQihjv0Ef2sV51J2YiAmboLcmE0adlEAv2c%253D%26q%3Dc%26srv%3Doauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion", + "pgp": "/static/federation/pgp/9D3D3BAF4744305EE3F0B837331AB575DD78D930.asc", + "nostr": "npub1mmfacq4p4xmpeevaz86fv5uuk073tuqry6sk73l9lrtkh2azf0dsykwxuj", + "website": null, + "fingerprint": "9D3D3BAF4744305EE3F0B837331AB575DD78D930" + }, + "badges": { + "isFounder": false, + "donatesToDevFund": 20, + "hasGoodOpSec": true, + "hasLargeLimits": false + }, + "policies": { + "Privacy Policy 1": "Dispute Evidence Submission: If a dispute arises, you will be asked to provide only the transaction-related documentation needed to evaluate your claim—for example, transaction IDs, payment‐confirmation screenshots, or other pertinent records. Please keep a record of all relevant information.", + "Privacy Policy 2": "Limited Retention Period: All sensitive data gathered for the purpose of resolving a dispute will be retained strictly for the duration of that process. Once the issue is fully resolved, this information will be irrevocably and securely deleted", + "Data Policy 1": "No Third-Party Data Sharing: RoboSats is deliberately designed to collect no user data—no IP addresses, geolocation, or personal identifiers—and will never share information with outside parties.", + "Data Policy 2": "Ephemeral Log Retention: All operational logs required to run the coordinator are automatically deleted after seven days, unless they’re essential for an active dispute.", + "Rule 1:": "Please do not share any personal information through the chat, if possible share as little revealing information as possible.", + "Rule 2:": "Email is provided for convenience but other means of communication are recommended. Like Nostr/SimpleX/Telegram" + }, + "mainnet": { + "onion": "http://45gzfolhp3dcfv6w7a4p2iwekvurdjcf4p2onhnmvyhauwxfsx7kguad.onion", + "clearnet": null, + "i2p": null + }, + "testnet": { + "onion": null, + "clearnet": null, + "i2p": null + }, + "mainnetNodesPubkeys": ["028059662c65626f3002290dd3151186bc1952726c03f56012406f3ceadb81cacc"], + "testnetNodesPubkeys": [], + "federated": true } }