Merge pull request #2091 from RoboSats/new-mobile-release

New mobile release
This commit is contained in:
KoalaSat
2025-07-20 13:33:36 +00:00
committed by GitHub
51 changed files with 862 additions and 1584 deletions

View File

@ -30,165 +30,146 @@ jobs:
- name: 'Checkout'
uses: actions/checkout@v4
# - name: 'Download Android Web.bundle Artifact (built frontend)'
# if: inputs.semver == '' # Only if workflow fired from frontend-build.yml
# uses: dawidd6/action-download-artifact@v11
# with:
# workflow: frontend-build.yml
# workflow_conclusion: success
# name: mobile-web.bundle
# path: mobile/html/Web.bundle
- name: 'Download Android Web.bundle Artifact (built frontend)'
if: inputs.semver == '' # Only if workflow fired from frontend-build.yml
uses: dawidd6/action-download-artifact@v11
with:
workflow: frontend-build.yml
workflow_conclusion: success
name: mobile-web.bundle
path: android/app/src/main/assets
# - name: 'Download main.js Artifact for a release'
# if: inputs.semver != '' # Only if fired as job in release.yml
# uses: actions/download-artifact@v4
# with:
# name: mobile-web.bundle
# path: mobile/html/Web.bundle
- name: 'Download main.js Artifact for a release'
if: inputs.semver != '' # Only if fired as job in release.yml
uses: actions/download-artifact@v4
with:
name: mobile-web.bundle
path: android/app/src/main/assets
# - name: 'Install npm Dependencies'
# run: |
# cd mobile
# npm install
- name: Cache gradle
uses: actions/cache@v4
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
restore-keys: |
${{ runner.os }}-gradle-
# - name: 'Patch modules' # react-native-tor and react-native-encrypted-storage rely on deprecated jcenter repositories. We patch the modules temporarily
# run: |
# cd mobile
# cp -r patch_modules/* node_modules/
# - uses: actions/setup-java@v4
# with:
# distribution: temurin
# java-version: 11
- name: Build APK
run: |
cd android
./gradlew assembleRelease --stacktrace
# - name: Setup Gradle
# uses: gradle/gradle-build-action@v3
- name: 'Check for non-FOSS libraries'
run: |
wget https://github.com/iBotPeaches/Apktool/releases/download/v2.7.0/apktool_2.7.0.jar
wget https://github.com/iBotPeaches/Apktool/raw/master/scripts/linux/apktool
# clone the repo
git clone https://gitlab.com/IzzyOnDroid/repo.git
# create a directory for Apktool and move the apktool* files there
mkdir -p repo/lib/radar/tool
mv apktool* repo/lib/radar/tool
# create an alias for ease of use
chmod u+x repo/lib/radar/tool/apktool
mv repo/lib/radar/tool/apktool_2.7.0.jar repo/lib/radar/tool/apktool.jar
repo/bin/scanapk.php android/app/build/outputs/apk/release/app-universal-release-unsigned.apk
# - name: Decode Keystore
# id: decode_keystore
# uses: timheuer/base64-to-file@v1.2
# with:
# fileName: 'keystore.jks'
# fileDir: './'
# encodedString: ${{ secrets.KEYSTORE }}
- name: Sign APK
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: android/app/build/outputs/apk/release
signingKeyBase64: ${{ secrets.KEYSTORE }}
alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASS }}
keyPassword: ${{ secrets.KEY_PASS }}
env:
BUILD_TOOLS_VERSION: "34.0.0"
# - name: 'Build Android Release'
# run: |
# cd mobile/android
# ./gradlew assembleRelease
# env:
# KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
# KEY_PASS: ${{ secrets.KEY_PASS }}
# KEY_STORE_PASS: ${{ secrets.KEY_STORE_PASS }}
- uses: kaisugi/action-regex-match@v1.0.1
id: regex-match
with:
text: ${{ github.ref }}
regex: '(v*-pre*)'
flags: gm
- name: 'Get Commit Hash'
id: commit
uses: pr-mpt/actions-commit-hash@v3
# - name: 'Check for non-FOSS libraries'
# run: |
# wget https://github.com/iBotPeaches/Apktool/releases/download/v2.7.0/apktool_2.7.0.jar
# wget https://github.com/iBotPeaches/Apktool/raw/master/scripts/linux/apktool
# # clone the repo
# git clone https://gitlab.com/IzzyOnDroid/repo.git
# # create a directory for Apktool and move the apktool* files there
# mkdir -p repo/lib/radar/tool
# mv apktool* repo/lib/radar/tool
# # create an alias for ease of use
# chmod u+x repo/lib/radar/tool/apktool
# mv repo/lib/radar/tool/apktool_2.7.0.jar repo/lib/radar/tool/apktool.jar
# repo/bin/scanapk.php mobile/android/app/build/outputs/apk/release/app-universal-release.apk
# Create app-universal-release APK artifact asset for Release
- name: 'Upload universal .apk Artifact'
if: inputs.semver != ''
uses: actions/upload-artifact@v4
with:
name: robosats-${{ inputs.semver }}-universal.apk
path: android/app/build/outputs/apk/release/app-universal-release-unsigned-signed.apk
# - name: 'Get Commit Hash'
# id: commit
# uses: pr-mpt/actions-commit-hash@v3
# Create app-arm64-v8a-release APK artifact asset for Release
- name: 'Upload arm64-v8a .apk Artifact'
if: inputs.semver != ''
uses: actions/upload-artifact@v4
with:
name: robosats-${{ inputs.semver }}-arm64-v8a.apk
path: android/app/build/outputs/apk/release/app-arm64-v8a-release-unsigned-signed.apk
# # Create artifacts (only for Release)
# # Create app-universal-release APK artifact asset for Release
# - name: 'Upload universal .apk Release Artifact (for Release)'
# uses: actions/upload-artifact@v4
# if: inputs.semver != '' # If this workflow is called from release.yml
# with:
# name: robosats-${{ inputs.semver }}-universal.apk
# path: mobile/android/app/build/outputs/apk/release/app-universal-release.apk
# Create app-armeabi-v7a-release APK artifact asset for Release
- name: 'Upload armeabi-v7a .apk Artifact'
if: inputs.semver != ''
uses: actions/upload-artifact@v4
with:
name: robosats-${{ inputs.semver }}-armeabi-v7a.apk
path: android/app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned-signed.apk
# # Create app-arm64-v8a-release APK artifact asset for Release
# - name: 'Upload arm64-v8a .apk Release Artifact (for Release)'
# uses: actions/upload-artifact@v4
# if: inputs.semver != '' # If this workflow is called from release.yml
# with:
# name: robosats-${{ inputs.semver }}-arm64-v8a.apk
# path: mobile/android/app/build/outputs/apk/release/app-arm64-v8a-release.apk
# Create app-x86_64-release APK artifact asset for Release
- name: 'Upload x86_64 .apk Artifact'
if: inputs.semver != ''
uses: actions/upload-artifact@v4
with:
name: robosats-${{ inputs.semver }}-x86_64.apk
path: android/app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk
# # Create app-armeabi-v7a-release APK artifact asset for Release
# - name: 'Upload armeabi-v7a .apk Release Artifact (for Release)'
# uses: actions/upload-artifact@v4
# if: inputs.semver != '' # If this workflow is called from release.yml
# with:
# name: robosats-${{ inputs.semver }}-armeabi-v7a.apk
# path: mobile/android/app/build/outputs/apk/release/app-armeabi-v7a-release.apk
- name: Create Pre-Release
id: create_release
if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release)
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: true
# # Create app-x86_64-release APK artifact asset for Release
# - name: 'Upload x86_64 .apk Release Artifact (for Release)'
# uses: actions/upload-artifact@v4
# if: inputs.semver != '' # If this workflow is called from release.yml
# with:
# name: robosats-${{ inputs.semver }}-x86_64.apk
# path: mobile/android/app/build/outputs/apk/release/app-x86_64-release.apk
- name: Upload APK Universal Asset
id: upload-release-asset-universal-apk
if: inputs.semver == ''
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: android/app/build/outputs/apk/release/app-universal-release-unsigned-signed.apk
asset_name: robosats-${{ github.ref }}-universal.apk
asset_content_type: application/zip
# - name: 'Create Pre-release'
# id: create_release
# if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release)
# uses: ncipollo/release-action@v1.18.0
# with:
# tag: android-${{ steps.commit.outputs.short }}
# name: robosats-android-${{ steps.commit.outputs.short }}
# prerelease: true
- name: Upload APK arm64-v8a Asset
id: upload-release-asset-arm64-v8a
if: inputs.semver == ''
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: android/app/build/outputs/apk/release/app-arm64-v8a-release-unsigned-signed.apk
asset_name: robosats-${{ github.ref }}-arm64-v8a.apk
asset_content_type: application/zip
# # Upload universal APK to pre-release
# - name: 'Upload universal Pre-release APK Asset'
# id: upload-release-universal-apk-asset
# if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release)
# uses: actions/upload-release-asset@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# upload_url: ${{ steps.create_release.outputs.upload_url }}
# asset_path: ./mobile/android/app/build/outputs/apk/release/app-universal-release.apk
# asset_name: robosats-${{ steps.commit.outputs.short }}-universal.apk
# asset_content_type: application/apk
# # Upload arm64-v8a APK to pre-release
# - name: 'Upload arm64-v8a Pre-release APK Asset'
# id: upload-release-arm64-v8a-apk-asset
# if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release)
# uses: actions/upload-release-asset@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# upload_url: ${{ steps.create_release.outputs.upload_url }}
# asset_path: ./mobile/android/app/build/outputs/apk/release/app-arm64-v8a-release.apk
# asset_name: robosats-${{ steps.commit.outputs.short }}-arm64-v8a.apk
# asset_content_type: application/apk
# # Upload armeabi-v7a APK to pre-release
# - name: 'Upload armeabi-v7a Pre-release APK Asset'
# id: upload-release-armeabi-v7a-apk-asset
# if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release)
# uses: actions/upload-release-asset@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# upload_url: ${{ steps.create_release.outputs.upload_url }}
# asset_path: ./mobile/android/app/build/outputs/apk/release/app-armeabi-v7a-release.apk
# asset_name: robosats-${{ steps.commit.outputs.short }}-armeabi-v7a.apk
# asset_content_type: application/apk
# # Upload x86_64 APK to pre-release
# - name: 'Upload x86_64 Pre-release APK Asset'
# id: upload-release-x86_64-apk-asset
# if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release)
# uses: actions/upload-release-asset@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# upload_url: ${{ steps.create_release.outputs.upload_url }}
# asset_path: ./mobile/android/app/build/outputs/apk/release/app-x86_64-release.apk
# asset_name: robosats-${{ steps.commit.outputs.short }}-x86_64.apk
# asset_content_type: application/apk
- name: Upload APK armeabi-v7a Asset
id: upload-release-asset-armeabi-v7a
if: inputs.semver == ''
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: android/app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned-signed.apk
asset_name: robosats-${{ github.ref }}-armeabi-v7a.apk
asset_content_type: application/zip

View File

@ -73,11 +73,11 @@ jobs:
path: |
web/static
web/*.html
# - name: 'Archive Mobile Build Results'
# uses: actions/upload-artifact@v4
# with:
# name: mobile-web.bundle
# path: mobile/html/Web.bundle
- name: 'Archive Mobile Build Results'
uses: actions/upload-artifact@v4
with:
name: mobile-web.bundle
path: android/app/src/main/assets
# Invoke pre-release image build if this was not a tag push
# Docker images tagged only with short commit hash

View File

@ -13,8 +13,8 @@ android {
applicationId = "com.robosats"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
versionCode = 15
versionName = "0.8.1-alpha"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@ -2,8 +2,7 @@ package com.robosats
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.graphics.Bitmap
import android.content.pm.ActivityInfo
import android.os.Build
import android.os.Bundle
import android.util.Log
@ -14,7 +13,6 @@ import android.webkit.GeolocationPermissions
import android.webkit.PermissionRequest
import android.webkit.ServiceWorkerController
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebSettings
@ -26,11 +24,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import com.robosats.tor.TorKmp
import com.robosats.tor.TorKmpManager
import java.io.ByteArrayInputStream
import java.net.HttpURLConnection
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.URL
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
@ -38,16 +31,12 @@ class MainActivity : AppCompatActivity() {
private lateinit var loadingContainer: ConstraintLayout
private lateinit var statusTextView: TextView
// Security constants
private val ALLOWED_DOMAINS = arrayOf(".onion")
private val CONTENT_SECURITY_POLICY = "default-src 'self'; connect-src 'self' https://*.onion http://*.onion; " +
"script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data:; font-src 'self' data:; object-src 'none'; " +
"media-src 'none'; frame-src 'none'; worker-src 'self';"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Lock the screen orientation to portrait mode
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
// We don't need edge-to-edge since we're using fitsSystemWindows
setContentView(R.layout.activity_main)
@ -158,7 +147,7 @@ class MainActivity : AppCompatActivity() {
return
}
// IMMEDIATELY set a blocking WebViewClient to prevent ANY network access
// Set a blocking WebViewClient to prevent ANY network access
webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
// Block ALL requests until we're sure Tor proxy is correctly set up
@ -166,7 +155,7 @@ class MainActivity : AppCompatActivity() {
}
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
// Block ALL URL loading attempts until proxy is properly configured
// Block ALL URL loading attempts
return true
}
}
@ -187,24 +176,12 @@ class MainActivity : AppCompatActivity() {
throw SecurityException("Tor disconnected during proxy setup")
}
// Try to set up the proxy
setupProxyForWebView()
// If we get here, proxy setup was successful
// Perform one final Tor connection check
if (!torKmp.isConnected()) {
throw SecurityException("Tor disconnected after proxy setup")
}
// Now get the proxy information that we previously verified in setupProxyForWebView
// Use system properties that we've already set up and verified
val proxyHost = System.getProperty("http.proxyHost")
?: throw SecurityException("Missing proxy host in system properties")
val proxyPort = System.getProperty("http.proxyPort")?.toIntOrNull()
?: throw SecurityException("Missing or invalid proxy port in system properties")
Log.d("WebViewProxy", "Using proxy settings: $proxyHost:$proxyPort")
// Success - now configure WebViewClient and load URL on UI thread
runOnUiThread {
updateStatus("Secure connection established. Loading app...")
@ -233,237 +210,6 @@ class MainActivity : AppCompatActivity() {
}
}
// Create a custom WebViewClient that forces all traffic through Tor
webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
// Verify Tor is connected before allowing any resource request
if (!torKmp.isConnected()) {
Log.e("SecurityError", "Tor disconnected during resource request")
return WebResourceResponse("text/plain", "UTF-8", null)
}
val urlString = request.url.toString()
Log.d("WebViewProxy", "Intercepting request: $urlString")
// Block all external requests that aren't to .onion domains or local files
if (!isAllowedRequest(urlString)) {
Log.e("SecurityPolicy", "Blocked forbidden request to: $urlString")
return WebResourceResponse("text/plain", "UTF-8", null)
}
try {
// Special handling for .onion domains
val isOnionDomain = urlString.contains(".onion")
// Only proceed if it's an onion domain or local file
if (!isOnionDomain && !urlString.startsWith("file://")) {
Log.e("SecurityPolicy", "Blocked non-onion external request: $urlString")
return WebResourceResponse("text/plain", "UTF-8", null)
}
// For .onion domains, we must use SOCKS proxy type
val proxyType = if (isOnionDomain)
Proxy.Type.SOCKS
else
Proxy.Type.HTTP
// Create a proxy instance for Tor with the appropriate type
val torProxy = Proxy(
proxyType,
InetSocketAddress(proxyHost, proxyPort)
)
if (isOnionDomain) {
Log.d("WebViewProxy", "Handling .onion domain with SOCKS proxy: $urlString")
}
// If it's a local file, return it directly
if (urlString.startsWith("file://")) {
// Let the system handle local files
return super.shouldInterceptRequest(view, request)
}
// Create connection with proxy already configured
val url = URL(urlString)
val connection = url.openConnection(torProxy)
// Configure basic connection properties
connection.connectTimeout = 60000 // Longer timeout for onion domains
connection.readTimeout = 60000
if (connection is HttpURLConnection) {
// Ensure no connection reuse to prevent proxy leaks
connection.setRequestProperty("Connection", "close")
// Add security headers
connection.setRequestProperty("Sec-Fetch-Site", "same-origin")
connection.setRequestProperty("Sec-Fetch-Mode", "cors")
connection.setRequestProperty("DNT", "1") // Do Not Track
// Copy request headers
request.requestHeaders.forEach { (key, value) ->
connection.setRequestProperty(key, value)
}
// Set the request method
connection.requestMethod = request.method
// Special handling for OPTIONS (CORS preflight) requests
if (request.method == "OPTIONS") {
// For OPTIONS, we'll create a custom response without making a network request
// This is the most reliable way to handle CORS preflight
Log.d("CORS", "Handling OPTIONS preflight request for: $urlString")
// Create CORS headers map
val preflightHeaders = HashMap<String, String>()
preflightHeaders["Access-Control-Allow-Origin"] = "*"
preflightHeaders["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, PUT, DELETE, HEAD"
preflightHeaders["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Authorization"
preflightHeaders["Access-Control-Max-Age"] = "86400" // Cache preflight for 24 hours
preflightHeaders["Access-Control-Allow-Credentials"] = "true"
preflightHeaders["Content-Type"] = "text/plain"
// Log CORS headers for debugging
Log.d("CORS", "Preflight response with CORS headers: $preflightHeaders")
// Return a custom preflight response without actually connecting
return WebResourceResponse(
"text/plain",
"UTF-8",
200,
"OK",
preflightHeaders,
ByteArrayInputStream("".toByteArray())
)
}
// Try to connect
connection.connect()
val responseCode = connection.responseCode
// Get content type
val mimeType = connection.contentType ?: "text/plain"
val encoding = connection.contentEncoding ?: "UTF-8"
Log.d("WebViewProxy", "Successfully proxied request to $url (HTTP ${connection.responseCode})")
// Get the correct input stream based on response code
val inputStream = if (responseCode >= 400) {
connection.errorStream ?: ByteArrayInputStream(byteArrayOf())
} else {
connection.inputStream
}
// Create response headers map with security headers
val responseHeaders = HashMap<String, String>()
// First copy original response headers, but carefully handle CORS headers
for (i in 0 until connection.headerFields.size) {
val key = connection.headerFields.keys.elementAtOrNull(i)
if (key != null && key.isNotEmpty()) {
// Skip any CORS headers from the original response - we'll add our own
if (!key.startsWith("Access-Control-")) {
val value = connection.getHeaderField(key)
if (value != null) {
responseHeaders[key] = value
}
} else {
// Log any CORS headers we're skipping from the original response
Log.d("CORS", "Skipping original CORS header: $key: ${connection.getHeaderField(key)}")
}
}
}
// Add our own CORS headers
responseHeaders["Access-Control-Allow-Origin"] = "*"
responseHeaders["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, PUT, DELETE, HEAD"
responseHeaders["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Authorization"
responseHeaders["Access-Control-Allow-Credentials"] = "true"
if (!responseHeaders.containsKey("Content-Security-Policy")) {
responseHeaders["Content-Security-Policy"] = CONTENT_SECURITY_POLICY
}
if (!responseHeaders.containsKey("X-Content-Type-Options")) {
responseHeaders["X-Content-Type-Options"] = "nosniff"
}
if (!responseHeaders.containsKey("X-Frame-Options")) {
responseHeaders["X-Frame-Options"] = "DENY"
}
if (!responseHeaders.containsKey("Referrer-Policy")) {
responseHeaders["Referrer-Policy"] = "no-referrer"
}
// Log the CORS headers for debugging
responseHeaders["Access-Control-Allow-Origin"]?.let {
Log.d("CORS", "Access-Control-Allow-Origin: $it")
}
// Return proxied response with security headers
return WebResourceResponse(
mimeType,
encoding,
responseCode,
"OK",
responseHeaders,
inputStream
)
} else {
// For non-HTTP connections (rare)
val inputStream = connection.getInputStream()
Log.d("WebViewProxy", "Successfully established non-HTTP connection to $url")
return WebResourceResponse(
"application/octet-stream",
"UTF-8",
inputStream
)
}
} catch (e: Exception) {
Log.e("WebViewProxy", "Error proxying request: $urlString - ${e.message}", e)
// For security, block the request rather than falling back to system handling
return WebResourceResponse("text/plain", "UTF-8", null)
}
}
// We're not handling SSL, so we don't need the onReceivedSslError method
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
// Verify Tor is still connected before allowing any request
if (!torKmp.isConnected()) {
Log.e("SecurityError", "Tor disconnected during navigation")
return true // Block the request
}
return false // Let our proxied client handle it
}
override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) {
Log.e("WebViewError", "Error loading resource: ${error.description}")
super.onReceivedError(view, request, error)
}
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
// Verify Tor is connected when page starts loading
if (!torKmp.isConnected()) {
Log.e("SecurityError", "Tor disconnected as page started loading")
view.stopLoading()
return
}
super.onPageStarted(view, url, favicon)
}
override fun onPageFinished(view: WebView, url: String) {
// Verify Tor is still connected when page finishes loading
if (!torKmp.isConnected()) {
Log.e("SecurityError", "Tor disconnected after page loaded")
return
}
// No JavaScript injection - just log page load completion
Log.d("WebView", "Page finished loading: $url")
super.onPageFinished(view, url)
}
}
webView.settings.userAgentString = "AndroidRobosats"
// Add the JavaScript interface
@ -488,46 +234,6 @@ class MainActivity : AppCompatActivity() {
}.start()
}
private fun setupProxyForWebView() {
// Triple-check Tor is connected
if (!torKmp.isConnected()) {
throw SecurityException("Cannot set up proxy - Tor is not connected")
}
try {
// Get the proxy from TorKmpManager, handling possible exceptions
val proxy = TorKmpManager.getTorKmpObject().proxy ?:
throw SecurityException("Tor proxy is null despite Tor being connected")
val inetSocketAddress = proxy.address() as InetSocketAddress
val host = inetSocketAddress.hostName
val port = inetSocketAddress.port
if (host.isBlank() || port <= 0) {
throw SecurityException("Invalid Tor proxy address: $host:$port")
}
Log.d("WebViewProxy", "Setting up Tor proxy: $host:$port")
// Set up the proxy
setWebViewProxy(applicationContext, host, port)
// Verify proxy was set correctly
if (System.getProperty("http.proxyHost") != host ||
System.getProperty("http.proxyPort") != port.toString()) {
throw SecurityException("Proxy verification failed - system properties don't match expected values")
}
Log.d("WebViewProxy", "Proxy setup completed successfully")
} catch (e: Exception) {
Log.e("WebViewProxy", "Error setting up proxy: ${e.message}", e)
throw SecurityException("Failed to set up Tor proxy: ${e.message}", e)
}
}
/**
* Sets the proxy for WebView using the most direct approach that's known to work with Tor
*/
/**
* Configure WebView settings with a security-first approach
*/
@ -597,24 +303,6 @@ class MainActivity : AppCompatActivity() {
webSettings.textZoom = 100
}
/**
* Check if a URL request is allowed based on security policy
*/
private fun isAllowedRequest(url: String): Boolean {
// Always allow local file requests
if (url.startsWith("file:///android_asset/") || url.startsWith("file:///data/")) {
return true
}
// Allow onion domains
if (ALLOWED_DOMAINS.any { url.contains(it) }) {
return true
}
// Block everything else
return false
}
// SSL error description method removed as we're not using SSL
/**
@ -638,57 +326,4 @@ class MainActivity : AppCompatActivity() {
super.onDestroy()
}
private fun setWebViewProxy(context: Context, proxyHost: String, proxyPort: Int) {
try {
// First set system properties (required as a foundation)
System.setProperty("http.proxyHost", proxyHost)
System.setProperty("http.proxyPort", proxyPort.toString())
System.setProperty("https.proxyHost", proxyHost)
System.setProperty("https.proxyPort", proxyPort.toString())
System.setProperty("proxy.host", proxyHost)
System.setProperty("proxy.port", proxyPort.toString())
Log.d("WebViewProxy", "Set system proxy properties")
// Create and apply a proxy at the application level
val proxyClass = Class.forName("android.net.ProxyInfo")
val proxyConstructor = proxyClass.getConstructor(String::class.java, Int::class.javaPrimitiveType, String::class.java)
val proxyInfo = proxyConstructor.newInstance(proxyHost, proxyPort, null)
try {
// Try to set global proxy through ConnectivityManager
val connectivityManager = context.getSystemService(CONNECTIVITY_SERVICE)
val setDefaultProxyMethod = connectivityManager.javaClass.getDeclaredMethod("setDefaultProxy", proxyClass)
setDefaultProxyMethod.isAccessible = true
setDefaultProxyMethod.invoke(connectivityManager, proxyInfo)
Log.d("WebViewProxy", "Set proxy via ConnectivityManager")
} catch (e: Exception) {
Log.w("WebViewProxy", "Could not set proxy via ConnectivityManager: ${e.message}")
}
// WebView operations must be run on the UI thread
runOnUiThread {
try {
// Force WebView to use proxy via direct settings (must be on UI thread)
webView.settings.javaClass.getDeclaredMethod("setHttpProxy", String::class.java, Int::class.javaPrimitiveType)
?.apply { isAccessible = true }
?.invoke(webView.settings, proxyHost, proxyPort)
Log.d("WebViewProxy", "Applied proxy directly to WebView settings")
} catch (e: Exception) {
Log.w("WebViewProxy", "Could not set proxy directly on WebView settings: ${e.message}")
// Continue - we'll rely on system properties and connection-level proxying
}
}
// Wait to ensure UI thread operations complete
// This prevents race conditions with WebView operations
Thread.sleep(500)
Log.d("WebViewProxy", "Proxy setup completed")
} catch (e: Exception) {
Log.e("WebViewProxy", "Error setting WebView proxy", e)
throw SecurityException("Failed to set WebView proxy: ${e.message}", e)
}
}
}

View File

@ -7,18 +7,24 @@ import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.widget.Toast
import com.robosats.tor.TorKmpManager.getTorKmpObject
import okhttp3.Call
import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.OkHttpClient.Builder
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okio.ByteString
import java.util.Objects
import org.json.JSONObject
import java.io.IOException
import java.util.concurrent.TimeUnit
import java.util.regex.Pattern
import okhttp3.Request.Builder as RequestBuilder
/**
* Provides a secure bridge between JavaScript and native Android code.
* This class is designed with security in mind, implementing input validation,
@ -238,6 +244,80 @@ class WebAppInterface(private val context: Context, private val webView: WebView
}
}
@JavascriptInterface
fun sendRequest(uuid: String, action: String, url: String, headers: String, body: String) {
// Validate inputs
if (!isValidUuid(uuid)) {
Log.e(TAG, "Invalid UUID for sendRequest: $uuid")
rejectPromise(uuid, "Invalid UUID")
return
}
try {
// Create OkHttpClient with Tor proxy
val client = Builder()
.connectTimeout(60, TimeUnit.SECONDS) // Set connection timeout
.readTimeout(30, TimeUnit.SECONDS) // Set read timeout
.proxy(getTorKmpObject().proxy)
.build()
// Build request with URL
val requestBuilder = RequestBuilder().url(url)
// Add headers from JSON
val headersObject = JSONObject(headers)
val keys = headersObject.keys()
while (keys.hasNext()) {
val key = keys.next()
val value = headersObject.optString(key)
requestBuilder.addHeader(key, value)
}
// Set request method and body
when (action) {
"DELETE" -> requestBuilder.delete()
"POST" -> {
val mediaType = "application/json; charset=utf-8".toMediaType()
val requestBody = body.toRequestBody(mediaType)
requestBuilder.post(requestBody)
}
else -> requestBuilder.get()
}
// Build and execute request
val request = requestBuilder.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.d("RobosatsError", e.toString())
rejectPromise(uuid, "Request failed: ${e.message}")
}
override fun onResponse(call: Call, response: Response) {
try {
// Get response body
val responseBody = response.body.string()
// Create JSON object with headers
val headersJson = JSONObject()
response.headers.names().forEach { name ->
headersJson.put(name, response.header(name))
}
// Return response as JSON string
val result = "{\"json\":$responseBody, \"headers\": $headersJson}"
resolvePromise(uuid, result)
} catch (e: Exception) {
Log.e(TAG, "Error processing response", e)
rejectPromise(uuid, "Error processing response: ${e.message}")
}
}
})
} catch (e: Exception) {
Log.e(TAG, "Error in sendRequest", e)
rejectPromise(uuid, "Error sending request: ${e.message}")
}
}
private fun onWsMessage(path: String?, message: String?) {
safeEvaluateJavascript("javascript:window.AndroidRobosats.onWSMessage('$path', '$message')")
}

View File

@ -23,11 +23,9 @@ import io.matthewnelson.kmp.tor.manager.common.state.isOff
import io.matthewnelson.kmp.tor.manager.common.state.isOn
import io.matthewnelson.kmp.tor.manager.common.state.isStarting
import io.matthewnelson.kmp.tor.manager.common.state.isStopping
import io.matthewnelson.kmp.tor.manager.R
import kotlinx.coroutines.*
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.ExecutionException
class TorKmp(application : Application) {
@ -234,14 +232,11 @@ class TorKmp(application : Application) {
if (seconds == null) {
it
} else {
appContext.getString(
R.string.kmp_tor_newnym_rate_limited,
seconds
)
TorControlSignal.NEW_NYM_RATE_LIMITED
}
}
it == TorControlSignal.NEW_NYM_SUCCESS -> {
appContext.getString(R.string.kmp_tor_newnym_success)
TorControlSignal.NEW_NYM_SUCCESS
}
else -> {
null

View File

@ -1,3 +1,3 @@
<resources>
<string name="app_name">Robosats</string>
</resources>
</resources>

46
android/zapstore.yaml Normal file
View File

@ -0,0 +1,46 @@
name: Robosats
summary: A simple and private bitcoin exchange
description: >-
RoboSats is a simple and private app to exchange bitcoin for national currencies.
Robosats simplifies the P2P user experience and uses lightning hold invoices to
minimize custody and trust requirements. The deterministically generated robot
avatars help users stick to best privacy practices.
Features:
- Decentralized: Scrow services are individually provided by members of the Robosats federation.
- Privacy focused: your robot avatar is deterministically generated, no need for registration.
- More than 10 languages available and over 60 fiat currencies
- Safe: simply lock a lightning hodl invoice and show you are real and committed.
- No data collection. Your communication with your peer is PGP encrypted, only you can read it.
- Lightning fast: the average sovereign trade finishes in ~ 8 minutes. Faster than a single block confirmation!
- Fully collateralized escrow: your peer is always committed and cannot run away with the funds.
- Strong incentives system: attempts of cheating are penalized with the slashing of the Sats in the fidelity bond.
- Guides and video tutorials available at https://learn.robosats.com/watch/en
You can join other cool Robots and get community support at https://learn.robosats.org.
repository: https://github.com/RoboSats/robosats
images:
- ../../fastlane/metadata/en-US/images/phoneScreenshots/01.jpg
- ../../fastlane/metadata/en-US/images/phoneScreenshots/02.jpg
- ../../fastlane/metadata/en-US/images/phoneScreenshots/03.jpg
- ../../fastlane/metadata/en-US/images/phoneScreenshots/04.jpg
- ../../fastlane/metadata/en-US/images/phoneScreenshots/05.jpg
- ../../fastlane/metadata/en-US/images/phoneScreenshots/06.jpg
icon: ../../fastlane/metadata/en-US/images/icon.png
tags: privacy lightning bitcoin p2p tor exchange foss
license: AGPL-3.0
remote_metadata:
- github
assets: # for github
- robosats-v\d+\.\d+\.\d+.\w+-universal.apk
- robosats-v\d+\.\d+\.\d+.\w+-arm64-v8a.apk
- robosats-v\d+\.\d+\.\d+.\w+-armeabi-v7a.apk

View File

@ -1,5 +1,6 @@
<p>RoboSats is a simple and private app to exchange bitcoin for national currencies. Robosats simplifies the P2P user experience and uses lightning hold invoices to minimize custody and trust requirements. The deterministically generated robot avatars help users stick to best privacy practices.</p>
<p><br><b>Features:</b></p><ul>
<li>Decentralized: Scrow services are individually provided by members of the Robosats federation.</li>
<li>Privacy focused: your robot avatar is deterministically generated, no need for registration.</li>
<li>More than 10 languages available and over 60 fiat currencies</li>
<li>Safe: simply lock a lightning hodl invoice and show you are real and committed.</li>
@ -9,4 +10,4 @@
<li>Strong incentives system: attempts of cheating are penalized with the slashing of the Sats in the fidelity bond.</li>
<li>Guides and video tutorials available at https://learn.robosats.org/watch/en</li>
</ul>
<p>You can join other cool Robots and get community support at <a href="https://t.me/robosats">our Telegram group</a>.</p>
<p>You can join other cool Robots and get community support at <a href="https://learn.robosats.org">https://learn.robosats.org</a>.</p>

View File

@ -41,7 +41,7 @@ const RobotProfile = ({
setView,
width,
}: RobotProfileProps): React.JSX.Element => {
const { windowSize, client, setOpen, open, navigateToPage } =
const { windowSize, setOpen, open, navigateToPage, client } =
useContext<UseAppStoreType>(AppContext);
const { garage, slotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
@ -226,7 +226,7 @@ const RobotProfile = ({
<Tooltip
placement='top'
enterTouchDelay={0}
hidden={!slot?.lastOrder?.id}
disableHoverListener={!slot?.lastOrder?.id}
title={t(
'Reusing trading identity degrades your privacy against other users, coordinators and observers.',
)}
@ -262,7 +262,20 @@ const RobotProfile = ({
>
<Grid container direction='column' alignItems='center' spacing={2} padding={2}>
<Grid item sx={{ width: '100%' }}>
<Typography variant='caption'>{t('Robot Garage')}</Typography>
<Grid container direction='row' justifyContent='space-between'>
<Typography variant='caption'>{t('Robot Garage')}</Typography>
{client !== 'mobile' && (
<Button
size='small'
color='primary'
onClick={() => {
garage.download();
}}
>
<Download style={{ width: '0.6em', height: '0.6em' }} />
</Button>
)}
</Grid>
<Select
error={!slot?.activeOrder?.id && Boolean(slot?.lastOrder?.id)}
fullWidth
@ -311,7 +324,7 @@ const RobotProfile = ({
</Select>
</Grid>
<Grid item container direction='row' alignItems='center' justifyContent='space-evenly'>
<Grid item container direction='row' justifyContent='space-between' width='100%'>
<Grid item>
<LoadingButton
loading={loading}
@ -319,32 +332,10 @@ const RobotProfile = ({
onClick={handleAddRobot}
size='large'
>
<Add /> <div style={{ width: '0.5em' }} />
<Add />
{!mobileView && t('Add Robot')}
</LoadingButton>
</Grid>
{client !== 'mobile' ? (
<Grid item>
<Button
size='large'
color='primary'
onClick={() => {
garage.download();
}}
>
<Download />
</Button>
</Grid>
) : null}
<Grid item>
<Button color='primary' onClick={handleDeleteRobot} size='large'>
<DeleteSweep /> <div style={{ width: '0.5em' }} />
{!mobileView && t('Delete Robot')}
</Button>
</Grid>
<Grid item>
<Button
color='primary'
@ -355,8 +346,13 @@ const RobotProfile = ({
});
}}
>
<Key /> <div style={{ width: '0.5em' }} />
{!mobileView && t('Recovery')}
<Key />
</Button>
</Grid>
<Grid item>
<Button color='primary' onClick={handleDeleteRobot} size='large'>
<DeleteSweep />
{!mobileView && t('Delete Robot')}
</Button>
</Grid>
</Grid>

View File

@ -28,6 +28,7 @@ const Coordinators = (): React.JSX.Element => {
}}
aria-labelledby='recovery-dialog-title'
aria-describedby='recovery-description'
fullWidth
>
<DialogContent>
<Grid container direction='column' alignItems='center' spacing={1} padding={2}>

View File

@ -241,6 +241,7 @@ const TopBar = (): React.JSX.Element => {
<ListItemText primary={t('Client info')} />
</ListItemButton>
</ListItem>
<div style={{ flexGrow: 1 }} />
<ListItem disablePadding>
<ListItemButton onClick={() => changePage('settings')}>
<ListItemIcon>
@ -249,7 +250,6 @@ const TopBar = (): React.JSX.Element => {
<ListItemText primary={t('Settings')} />
</ListItemButton>
</ListItem>
<div style={{ flexGrow: 1 }} />
{client === 'mobile' && (
<ListItem disablePadding sx={{ display: 'flex', flexDirection: 'column' }}>
<ListItemButton selected>

View File

@ -88,7 +88,7 @@ const AuditPGPDialog = ({
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const slot = garage.getSlotByOrder(order.shortAlias, order.id);
console.log(slot);
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>{t("Don't trust, verify")}</DialogTitle>

View File

@ -1,24 +1,22 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
Dialog,
DialogContent,
Divider,
List,
ListItemAvatar,
ListItemText,
ListItem,
Typography,
LinearProgress,
Select,
Grid,
MenuItem,
SelectChangeEvent,
} from '@mui/material';
import BoltIcon from '@mui/icons-material/Bolt';
import RobotAvatar from '../RobotAvatar';
import RobotInfo from '../RobotInfo';
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
import { type Coordinator } from '../../models';
import { Slot, type Coordinator } from '../../models';
interface Props {
open: boolean;
@ -27,16 +25,21 @@ interface Props {
const ProfileDialog = ({ open = false, onClose }: Props): React.JSX.Element => {
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const { garage, slotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation();
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
setLoading(!garage.getSlot()?.hashId);
}, [slotUpdatedAt]);
loadRobot(garage.currentSlot ?? '');
}, []);
const slot = garage.getSlot();
const loadRobot = (token: string) => {
garage.setCurrentSlot(token);
garage.fetchRobot(federation, garage.getSlot()?.token ?? '');
};
const handleChangeSlot = (e: SelectChangeEvent<number | 'loading'>): void => {
if (e?.target?.value) loadRobot(e.target.value as string);
};
return (
<Dialog
@ -44,70 +47,67 @@ const ProfileDialog = ({ open = false, onClose }: Props): React.JSX.Element => {
onClose={onClose}
aria-labelledby='profile-title'
aria-describedby='profile-description'
fullWidth
>
<div style={loading ? {} : { display: 'none' }}>
<LinearProgress />
</div>
<DialogContent>
<DialogContent style={{ width: '100%' }}>
<Typography component='h5' variant='h5'>
{t('Your Robot')}
</Typography>
<List>
<Divider />
<ListItem className='profileNickname'>
<ListItemText>
<Typography component='h6' variant='h6'>
<div style={{ position: 'relative', left: '-7px' }}>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'left',
flexWrap: 'wrap',
width: 300,
}}
>
<BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} />
<a>{slot?.nickname}</a>
<BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} />
</div>
</div>
</Typography>
</ListItemText>
<ListItemAvatar>
<RobotAvatar
avatarClass='profileAvatar'
style={{ width: 65, height: 65 }}
hashId={slot?.hashId ?? ''}
/>
</ListItemAvatar>
</ListItem>
<Divider />
</List>
<Select
fullWidth
inputProps={{
style: { textAlign: 'center' },
}}
value={garage.currentSlot}
onChange={handleChangeSlot}
>
{Object.values(garage.slots).map((slot: Slot, index: number) => {
return (
<MenuItem key={index} value={slot.token}>
<Grid
container
direction='row'
justifyContent='flex-start'
alignItems='center'
style={{ height: '2.8em' }}
spacing={1}
>
<Grid item>
<RobotAvatar
hashId={slot?.hashId}
smooth={true}
style={{ width: '2.6em', height: '2.6em' }}
placeholderType='loading'
small={true}
/>
</Grid>
<Grid item>
<Typography>{slot?.nickname}</Typography>
</Grid>
</Grid>
</MenuItem>
);
})}
</Select>
<Typography>
<b>{t('Coordinators that know your robot:')}</b>
</Typography>
<List
sx={{ width: '100%', bgcolor: 'background.paper' }}
sx={{
width: '100%',
bgcolor: 'background.paper',
maxHeight: '28em',
overflowY: 'auto',
}}
component='nav'
aria-labelledby='coordinators-list'
>
{federation.getCoordinators().map((coordinator: Coordinator): React.JSX.Element => {
const coordinatorRobot = garage.getSlot()?.getRobot(coordinator.shortAlias);
return (
<div key={coordinator.shortAlias}>
<RobotInfo
coordinator={coordinator}
onClose={onClose}
disabled={coordinatorRobot?.loading}
/>
<RobotInfo coordinator={coordinator} onClose={onClose} />
</div>
);
})}

View File

@ -51,13 +51,20 @@ const SelectCoordinator: React.FC<SelectCoordinatorProps> = ({
return (
<Grid item>
{coordinator?.info && !coordinator?.info?.swap_enabled && (
<Grid sx={{ marginBottom: 1 }}>
<Alert severity='warning' sx={{ marginTop: 2 }}>
{t('This coordinator does not support on-chain swaps.')}
</Alert>
</Grid>
)}
<Grid sx={{ marginBottom: 1 }}>
<Alert
severity={
coordinator?.info ? (coordinator?.info?.swap_enabled ? 'success' : 'warning') : 'info'
}
sx={{ marginTop: 2 }}
>
{coordinator?.info
? coordinator?.info?.swap_enabled
? t('Supports on-chain swaps.')
: t('Does not support on-chain swaps.')
: t('Loading coordinator info...')}
</Alert>
</Grid>
<Box
sx={{
backgroundColor: 'background.paper',

View File

@ -294,7 +294,7 @@ const OrderDetails = ({
? coordinator?.info?.swap_enabled
? t('Supports on-chain swaps.')
: t('Does not support on-chain swaps.')
: t('Loading cooridnator info...')}
: t('Loading coordinator info...')}
</Alert>
</Grid>
</ListItem>

View File

@ -23,12 +23,11 @@ import {
} from '@mui/material';
import { Numbers, Send, EmojiEvents } from '@mui/icons-material';
import { useNavigate } from 'react-router-dom';
import { type Coordinator } from '../../models';
import { Robot, type Coordinator } from '../../models';
import { useTranslation } from 'react-i18next';
import { EnableTelegramDialog } from '../Dialogs';
import { UserNinjaIcon } from '../Icons';
import { getWebln } from '../../utils';
import { signCleartextMessage } from '../../pgp';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
@ -38,11 +37,10 @@ import RobotAvatar from '../RobotAvatar';
interface Props {
coordinator: Coordinator;
onClose: () => void;
disabled?: boolean;
}
const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) => {
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const RobotInfo: React.FC<Props> = ({ coordinator, onClose }: Props) => {
const { garage, slotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { setOpen, navigateToPage } = useContext<UseAppStoreType>(AppContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const navigate = useNavigate();
@ -55,38 +53,19 @@ const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) =
const [withdrawn, setWithdrawn] = useState<boolean>(false);
const [badInvoice, setBadInvoice] = useState<string>('');
const [openClaimRewards, setOpenClaimRewards] = useState<boolean>(false);
const [weblnEnabled, setWeblnEnabled] = useState<boolean>(false);
const [openEnableTelegram, setOpenEnableTelegram] = useState<boolean>(false);
const [openOptions, setOpenOptions] = useState<boolean>(false);
const robot = garage.getSlot()?.getRobot(coordinator.shortAlias);
const handleWebln = async (): Promise<void> => {
void getWebln()
.then(() => {
setWeblnEnabled(true);
})
.catch(() => {
setWeblnEnabled(false);
console.log('WebLN not available');
});
};
const [disabled, setDisable] = useState<boolean>(false);
const [robot, setRobot] = useState<Robot | null>(null);
useEffect(() => {
void handleWebln();
}, []);
const robot = garage.getSlot()?.getRobot(coordinator.shortAlias) ?? null;
setRobot(robot);
}, [slotUpdatedAt]);
const handleWeblnInvoiceClicked = async (e: MouseEvent<HTMLButtonElement, MouseEvent>): void => {
e.preventDefault();
if (robot != null && robot.earnedRewards > 0) {
const webln = await getWebln();
const invoice = webln.makeInvoice(robot.earnedRewards).then(() => {
if (invoice != null) {
handleSubmitInvoiceClicked(e, invoice.paymentRequest);
}
});
}
};
useEffect(() => {
setDisable(Boolean(robot?.loading));
}, [robot?.loading]);
const handleSubmitInvoiceClicked = (e: Event, rewardInvoice: string): void => {
setBadInvoice('');
@ -134,7 +113,7 @@ const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) =
) : robot?.lastOrderId ? (
<Typography color='warning'>&nbsp;{t('Finished order')}</Typography>
) : (
<Typography>{t('No existing orders found')}</Typography>
<Typography>{t('No orders found')}</Typography>
)
}
/>
@ -339,26 +318,6 @@ const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) =
</Button>
</Grid>
</Grid>
{weblnEnabled ? (
<Grid container style={{ display: 'flex', alignItems: 'stretch' }}>
<Grid item alignItems='stretch' style={{ display: 'flex', maxWidth: 240 }}>
<Button
sx={{ maxHeight: 38, minWidth: 230 }}
onClick={(e) => {
handleWeblnInvoiceClicked(e);
}}
variant='contained'
color='primary'
size='small'
type='submit'
>
{t('Generate with Webln')}
</Button>
</Grid>
</Grid>
) : (
<></>
)}
</form>
)}
</ListItem>

View File

@ -7,7 +7,6 @@ import {
Collapse,
Link,
Alert,
AlertTitle,
Tooltip,
IconButton,
Button,
@ -15,7 +14,7 @@ import {
} from '@mui/material';
import currencies from '../../../../static/assets/currencies.json';
import TradeSummary from '../TradeSummary';
import { Favorite, RocketLaunch, ContentCopy, Refresh, Info } from '@mui/icons-material';
import { Favorite, RocketLaunch, ContentCopy, Refresh } from '@mui/icons-material';
import { LoadingButton } from '@mui/lab';
import { finalizeEvent, type Event } from 'nostr-tools';
import { type Order } from '../../../models';
@ -26,6 +25,7 @@ import {
} from '../../../contexts/FederationContext';
import { type UseAppStoreType, AppContext } from '../../../contexts/AppContext';
import { GarageContext, type UseGarageStoreType } from '../../../contexts/GarageContext';
import { useTheme } from '@mui/system';
interface SuccessfulPromptProps {
order: Order;
@ -43,6 +43,7 @@ export const SuccessfulPrompt = ({
loadingRenew,
}: SuccessfulPromptProps): React.JSX.Element => {
const { t } = useTranslation();
const theme = useTheme();
const currencyCode: string = currencies[`${order.currency}`];
const { settings } = useContext<UseAppStoreType>(AppContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
@ -50,7 +51,6 @@ export const SuccessfulPrompt = ({
const [coordinatorToken, setCoordinatorToken] = useState<string>();
const [hostRating, setHostRating] = useState<number>();
const [tokenError, setTokenError] = useState<boolean>(false);
const rateHostPlatform = function (): void {
if (!hostRating) return;
@ -60,7 +60,6 @@ export const SuccessfulPrompt = ({
if (!coordinatorToken) {
console.error('Missing coordinator token');
setTokenError(true);
return;
}
@ -108,48 +107,50 @@ export const SuccessfulPrompt = ({
spacing={0.5}
padding={1}
>
<Grid item xs={12}>
<Typography variant='body2' align='center'>
{t('Rate your trade experience')}
</Typography>
</Grid>
<Grid item>
<Rating
name='size-large'
defaultValue={0}
size='large'
onChange={(e) => {
const rate = e.target.value;
rateUserPlatform(rate);
}}
/>
</Grid>
<Grid item xs={12}>
<Typography variant='body2' align='center'>
{t('Rate your host')} <b>{federation.getCoordinator(order.shortAlias)?.longAlias}</b>{' '}
<Tooltip title={t('You need to enable nostr to rate your coordinator.')}>
<Info sx={{ width: 15 }} />
</Tooltip>
</Typography>
</Grid>
{tokenError && (
<Grid sx={{ marginBottom: 1 }}>
<Alert severity='error' sx={{ marginTop: 2 }}>
{t("Error obatining coordinator' s token.")}
</Alert>
<Grid container direction='row'>
<Grid item width='48%'>
<Typography variant='body2' align='center'>
{t('Rate your trade experience')}
</Typography>
<Rating
name='size-large'
defaultValue={0}
size='large'
onChange={(e) => {
const rate = e.target.value;
rateUserPlatform(rate);
}}
/>
</Grid>
<Grid item width='48%'>
<Tooltip
title={t('You need to enable nostr to rate your coordinator.')}
disableHoverListener={settings.connection === 'nostr'}
>
<div
style={{
borderLeft: `1px solid ${theme.palette.divider}`,
marginLeft: '6px',
paddingLeft: '6px',
}}
>
<Typography variant='body2' align='center'>
{t('Rate your host')}{' '}
<b>{federation.getCoordinator(order.shortAlias)?.longAlias}</b>{' '}
</Typography>
<Rating
disabled={settings.connection !== 'nostr'}
name='size-large'
defaultValue={0}
size='large'
onChange={(e) => {
const rate = e.target.value;
setHostRating(parseInt(rate));
}}
/>
</div>
</Tooltip>
</Grid>
)}
<Grid item>
<Rating
disabled={settings.connection !== 'nostr'}
name='size-large'
defaultValue={0}
size='large'
onChange={(e) => {
const rate = e.target.value;
setHostRating(parseInt(rate));
}}
/>
</Grid>
{hostRating ? (
<Grid item xs={12}>
@ -205,75 +206,45 @@ export const SuccessfulPrompt = ({
)}
{/* SHOW TXID IF USER RECEIVES ONCHAIN */}
<Collapse in={Boolean(order.txid)} sx={{ marginTop: 0.5 }}>
<Alert severity='success'>
<AlertTitle>
{t('Your TXID')}
<Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}>
<IconButton
color='inherit'
onClick={() => {
systemClient.copyToClipboard(order.txid);
}}
>
<ContentCopy sx={{ width: '1em', height: '1em' }} />
</IconButton>
</Tooltip>
</AlertTitle>
<Typography
variant='body2'
align='center'
sx={{ wordWrap: 'break-word', width: '15.71em' }}
>
<Link
target='_blank'
href={
'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/' +
(order.network === 'testnet' ? 'testnet/' : '') +
'tx/' +
order.txid
}
<Collapse in={Boolean(order.txid)} sx={{ width: '100%' }}>
<Alert
severity='success'
style={{ marginTop: 0.5 }}
action={
<IconButton
color='inherit'
onClick={() => {
systemClient.copyToClipboard(order.txid);
}}
>
{order.txid}
</Link>
</Typography>
<ContentCopy sx={{ width: '0.8em', height: '0.8em' }} />
</IconButton>
}
>
{t('Your TXID')}
</Alert>
</Collapse>
<Collapse
in={order.tx_queued && order.address !== undefined && order.txid == null}
sx={{ marginTop: 0.5 }}
sx={{ width: '100%' }}
>
<Alert severity='info'>
<AlertTitle>
<CircularProgress sx={{ maxWidth: '0.8em', maxHeight: '0.8em' }} />
<a> </a>
{t('Sending coins to')}
<Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}>
<IconButton
color='inherit'
onClick={() => {
systemClient.copyToClipboard(order.address);
}}
>
<ContentCopy sx={{ width: '0.8em', height: '0.8em' }} />
</IconButton>
</Tooltip>
</AlertTitle>
<Typography
variant='body2'
align='center'
sx={{ wordWrap: 'break-word', width: '15.71em' }}
>
<Link
target='_blank'
href={`http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/${
order.network === 'testnet' ? 'testnet/' : ''
}address/${order.address}`}
<Alert
sx={{ marginTop: 0.5 }}
severity='info'
action={
<IconButton
color='inherit'
onClick={() => {
systemClient.copyToClipboard(order.address);
}}
>
{order.address}
</Link>
</Typography>
<ContentCopy sx={{ width: '0.8em', height: '0.8em' }} />
</IconButton>
}
>
<CircularProgress sx={{ maxWidth: '0.8em', maxHeight: '0.8em', marginRight: '5px' }} />
{t('Sending coins')}
</Alert>
</Collapse>
@ -307,7 +278,7 @@ export const SuccessfulPrompt = ({
</Grid>
{order.platform_summary != null ? (
<Grid item>
<Grid item sx={{ marginTop: 0.5 }}>
<TradeSummary
robotNick={order.ur_nick}
isMaker={order.is_maker}

View File

@ -1,5 +1,4 @@
import React, { useContext, useState } from 'react';
import { format } from 'date-fns';
import { useTranslation } from 'react-i18next';
import {
Badge,
@ -20,7 +19,6 @@ import RobotAvatar from '../RobotAvatar';
// Icons
import {
Schedule,
PriceChange,
LockOpen,
AccountBalance,
@ -70,11 +68,6 @@ const TradeSummary = ({
const [buttonValue, setButtonValue] = useState<number>(isMaker ? 0 : 2);
const userSummary = buttonValue === 0 ? makerSummary : takerSummary;
const contractTimestamp = new Date(platformSummary.contract_timestamp ?? null);
const totalTime = platformSummary.contract_total_time;
const hours = parseInt(totalTime / 3600);
const mins = parseInt((totalTime - hours * 3600) / 60);
const secs = parseInt(totalTime - hours * 3600 - mins * 60);
const onClickExport = function (): void {
const summary = {
@ -100,6 +93,7 @@ const TradeSummary = ({
backgroundColor: theme.palette.background.paper,
borderRadius: '0.3em',
padding: '0.5em',
width: '19em',
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
@ -319,22 +313,6 @@ const TradeSummary = ({
secondary={t('Contract exchange rate')}
/>
</ListItem>
<ListItem>
<ListItemText
primary={format(contractTimestamp, 'do LLL HH:mm:ss')}
secondary={t('Timestamp')}
/>
<ListItemIcon>
<Schedule />
</ListItemIcon>
<ListItemText
primary={`${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}:${String(
secs,
).padStart(2, '0')}`}
secondary={t('Completed in')}
/>
</ListItem>
</List>
</div>
</Box>

View File

@ -10,7 +10,6 @@ import React, {
} from 'react';
import { defaultMaker, type Maker, Garage } from '../models';
import { systemClient } from '../services/System';
import { type UseAppStoreType, AppContext } from './AppContext';
import { type UseFederationStoreType, FederationContext } from './FederationContext';
@ -66,7 +65,7 @@ export const GarageContextProvider = ({
children,
}: GarageContextProviderProps): React.JSX.Element => {
// All garage data structured
const { settings, torStatus, open, page, client } = useContext<UseAppStoreType>(AppContext);
const { settings, torStatus, page } = useContext<UseAppStoreType>(AppContext);
const pageRef = useRef(page);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const [garage] = useState<Garage>(initialGarageContext.garage);
@ -97,31 +96,14 @@ export const GarageContextProvider = ({
}, []);
useEffect(() => {
if (client !== 'mobile' || torStatus === 'ON' || !settings.useProxy) {
const token = garage.getSlot()?.token;
if (token) void garage.fetchRobot(federation, token);
}
const token = garage.getSlot()?.token;
if (token) void garage.fetchRobot(federation, token);
}, [settings.network, settings.useProxy, torStatus]);
useEffect(() => {
if (client === 'mobile' && !systemClient.loading) {
garage.loadSlots();
}
}, [systemClient.loading]);
useEffect(() => {
pageRef.current = page;
}, [page]);
// use effects to fetchRobots on Profile open
useEffect(() => {
const slot = garage.getSlot();
if (open.profile && slot?.hashId && slot?.token) {
void garage.fetchRobot(federation, slot?.token); // refresh/update existing robot
}
}, [open.profile]);
const fetchSlotActiveOrder: () => void = () => {
const slot = garage?.getSlot();
if (slot?.activeOrder?.id) {

View File

@ -28,8 +28,8 @@ class Slot {
const { hasEnoughEntropy, bitsEntropy, shannonEntropy } = validateTokenEntropy(token);
const tokenSHA256 = hexToBase91(sha256(token));
const nostrHash = sha256Hash(sha512(this.token));
this.nostrSecKey = nostrHash;
const nostrSecKey = sha256Hash(sha512(this.token));
this.nostrSecKey = nostrSecKey;
const nostrPubKey = getPublicKey(this.nostrSecKey);
this.nostrPubKey = nostrPubKey;
@ -167,6 +167,7 @@ class Slot {
token: defaultRobot.token,
pubKey: defaultRobot.pubKey,
encPrivKey: defaultRobot.encPrivKey,
nostrPubKey: defaultRobot.nostrPubKey,
});
void this.robots[shortAlias].fetch(federation);
this.updateSlotFromRobot(this.robots[shortAlias]);

View File

@ -13,6 +13,13 @@ interface AndroidAppRobosats {
getTorStatus: (uuid: string) => void;
openWS: (uuid: string, path: string) => void;
sendWsMessage: (uuid: string, path: string, message: string) => void;
sendRequest: (
uuid: string,
action: 'GET' | 'POST' | 'DELETE',
url: string,
headers: string,
body: string,
) => void;
}
class AndroidRobosats {

View File

@ -1,57 +0,0 @@
import type NativeRobosats from './index';
declare global {
interface Window {
ReactNativeWebView?: ReactNativeWebView;
NativeRobosats?: NativeRobosats;
RobosatsSettings: 'web-basic' | 'web-pro' | 'selfhosted-basic' | 'selfhosted-pro';
}
}
export interface ReactNativeWebView {
postMessage: (message: string) => Promise<Record<string, object>>;
}
export interface NativeWebViewMessageHttp {
id?: number;
category: 'http';
type: 'post' | 'get' | 'put' | 'delete';
path: string;
baseUrl: string;
headers?: object;
body?: object;
}
export interface NativeWebViewMessageSystem {
id?: number;
category: 'system';
type:
| 'init'
| 'torStatus'
| 'WsMessage'
| 'copyToClipboardString'
| 'setCookie'
| 'deleteCookie'
| 'navigateToPage';
key?: string;
detail?: string;
}
export interface NativeWebViewMessageRoboidentities {
id?: number;
category: 'roboidentities';
type: 'roboname' | 'robohash';
string?: string;
size?: string;
}
export declare type NativeWebViewMessage =
| NativeWebViewMessageHttp
| NativeWebViewMessageSystem
| NativeWebViewMessageRoboidentities
| NA;
export interface NativeRobosatsPromise {
resolve: (value: object | PromiseLike<object>) => void;
reject: (reason?: string) => void;
}

View File

@ -1,70 +0,0 @@
import {
type NativeRobosatsPromise,
type NativeWebViewMessage,
type NativeWebViewMessageSystem,
} from './index.d';
class NativeRobosats {
public torDaemonStatus = 'NOTINIT';
private messageCounter: number = 0;
private readonly pendingMessages = new Map<number, NativeRobosatsPromise>();
public cookies: Record<string, string> = {};
public loadCookie = (cookie: { key: string; value: string }): void => {
this.cookies[cookie.key] = cookie.value;
};
public onMessageResolve: (messageId: number, response?: object) => void = (
messageId,
response = {},
) => {
if (this.pendingMessages.has(messageId)) {
this.pendingMessages.get(messageId)?.resolve(response);
this.pendingMessages.delete(messageId);
}
};
public onMessageReject: (messageId: number, response?: object) => void = (
messageId,
response = {},
) => {
if (this.pendingMessages.has(messageId)) {
this.pendingMessages.get(messageId)?.reject(response);
this.pendingMessages.delete(messageId);
}
};
public onMessage: (message: NativeWebViewMessageSystem) => void = (message) => {
if (message.type === 'torStatus') {
this.torDaemonStatus = message.detail ?? 'ERROR';
window.dispatchEvent(new CustomEvent('torStatus', { detail: this.torDaemonStatus }));
} else if (message.type === 'setCookie') {
if (message.key !== undefined) {
this.cookies[message.key] = String(message.detail);
}
} else {
window.dispatchEvent(new CustomEvent(message.type, { detail: message?.detail }));
}
};
public postMessage: (message: NativeWebViewMessage) => Promise<object> = async (message) => {
this.messageCounter += 1;
message.id = this.messageCounter;
const json = JSON.stringify(message);
void window.ReactNativeWebView?.postMessage(json);
return await new Promise<object>((resolve, reject) => {
if (message.id !== undefined) {
this.pendingMessages.set(message.id, {
resolve,
reject,
});
}
});
};
}
export default NativeRobosats;

View File

@ -1,4 +0,0 @@
import RoboidentitiesClientNativeClient from './RoboidentitiesNativeClient';
import { type RoboidentitiesClient } from './type';
export const roboidentitiesClient: RoboidentitiesClient = new RoboidentitiesClientNativeClient();

View File

@ -1,42 +0,0 @@
import { type RoboidentitiesClient } from '../type';
class RoboidentitiesNativeClient implements RoboidentitiesClient {
private robonames: Record<string, string> = {};
private robohashes: Record<string, string> = {};
public generateRoboname: (initialString: string) => Promise<string> = async (initialString) => {
if (this.robonames[initialString]) {
return this.robonames[initialString];
} else {
const response = await window.NativeRobosats?.postMessage({
category: 'roboidentities',
type: 'roboname',
detail: initialString,
});
const result = response ? Object.values(response)[0] : '';
this.robonames[initialString] = result;
return result;
}
};
public generateRobohash: (initialString: string, size: 'small' | 'large') => Promise<string> =
async (initialString, size) => {
const key = `${initialString};${size === 'small' ? 80 : 256}`;
if (this.robohashes[key]) {
return this.robohashes[key];
} else {
const response = await window.NativeRobosats?.postMessage({
category: 'roboidentities',
type: 'robohash',
detail: key,
});
const result: string = response ? Object.values(response)[0] : '';
const image: string = `data:image/png;base64,${result}`;
this.robohashes[key] = image;
return image;
}
};
}
export default RoboidentitiesNativeClient;

View File

@ -1,64 +0,0 @@
import { type SystemClient } from '..';
import NativeRobosats from '../../Native';
class SystemNativeClient implements SystemClient {
constructor() {
window.NativeRobosats = new NativeRobosats();
void window.NativeRobosats.postMessage({
category: 'system',
type: 'init',
}).then(() => {
this.loading = false;
});
}
public loading = true;
public copyToClipboard: (value: string) => void = async (value) => {
return await window.NativeRobosats?.postMessage({
category: 'system',
type: 'copyToClipboardString',
detail: value,
});
};
public getCookie: (key: string) => string = (key) => {
const cookie = window.NativeRobosats?.cookies[key];
return cookie === null || cookie === undefined ? '' : cookie;
};
public setCookie: (key: string, value: string) => void = (key, value) => {
window.NativeRobosats?.loadCookie({ key, value });
void window.NativeRobosats?.postMessage({
category: 'system',
type: 'setCookie',
key,
detail: value,
});
};
public deleteCookie: (key: string) => void = (key) => {
delete window.NativeRobosats?.cookies[key];
void window.NativeRobosats?.postMessage({
category: 'system',
type: 'deleteCookie',
key,
});
};
// Emulate storage as emulated cookies (....to improve)
public getItem: (key: string) => string = (key) => {
return this.getCookie(key);
};
public setItem: (key: string, value: string) => void = (key, value) => {
this.setCookie(key, value);
};
public deleteItem: (key: string) => void = (key) => {
this.deleteCookie(key);
};
}
export default SystemNativeClient;

View File

@ -1,4 +1,3 @@
import SystemNativeClient from './SystemNativeClient';
import SystemWebClient from './SystemWebClient';
import SystemDesktopClient from './SystemDesktopClient';
import SystemAndroidClient from './SystemAndroidClient';
@ -15,11 +14,7 @@ export interface SystemClient {
}
function getSystemClient(): SystemClient {
if (window.navigator.userAgent.includes('robosats')) {
// If userAgent has "RoboSats", we assume the app is running inside of the
// react-native-web view of the RoboSats Android app.
return new SystemNativeClient();
} else if (window.navigator.userAgent.includes('Electron')) {
if (window.navigator.userAgent.includes('Electron')) {
// If userAgent has "Electron", we assume the app is running inside of an Electron app.
return new SystemDesktopClient();
} else if (window.navigator.userAgent.includes('AndroidRobosats')) {

View File

@ -1,91 +0,0 @@
import { WebsocketState, type WebsocketClient, type WebsocketConnection } from '..';
import WebsocketWebClient from '../WebsocketWebClient';
class WebsocketConnectionNative implements WebsocketConnection {
constructor(path: string) {
this.path = path;
window.addEventListener('wsMessage', (event) => {
const path: string = event?.detail?.path;
const message: string = event?.detail?.message;
if (path && message && path === this.path) {
this.wsMessagePromises.forEach((fn) => {
fn({ data: message });
});
}
});
}
private readonly path: string;
private readonly wsMessagePromises: Array<(message: object) => void> = [];
private readonly wsClosePromises: Array<() => void> = [];
public send: (message: string) => void = (message: string) => {
void window.NativeRobosats?.postMessage({
category: 'ws',
type: 'send',
path: this.path,
message,
});
};
public close: () => void = () => {
void window.NativeRobosats?.postMessage({
category: 'ws',
type: 'close',
path: this.path,
}).then((response) => {
if (response.connection) {
this.wsClosePromises.forEach((fn) => {
fn();
});
} else {
Error('Failed to close websocket connection.');
}
});
};
public onMessage: (event: (message: object) => void) => void = (event) => {
this.wsMessagePromises.push(event);
};
public onClose: (event: () => void) => void = (event) => {
this.wsClosePromises.push(event);
};
public onError: (event: (error: object) => void) => void = (_event) => {
// Not implemented
};
public getReadyState: () => number = () => WebsocketState.OPEN;
}
class WebsocketNativeClient implements WebsocketClient {
public useProxy = true;
private readonly webClient: WebsocketWebClient = new WebsocketWebClient();
public open: (path: string) => Promise<WebsocketConnection> = async (path) => {
if (!this.useProxy) return await this.webClient.open(path);
return await new Promise<WebsocketConnection>((resolve, reject) => {
window.NativeRobosats?.postMessage({
category: 'ws',
type: 'open',
path,
})
.then((response) => {
if (response.connection) {
resolve(new WebsocketConnectionNative(path));
} else {
reject(new Error('Failed to establish a websocket connection.'));
}
})
.catch(() => {
reject(new Error('Failed to establish a websocket connection.'));
});
});
};
}
export default WebsocketNativeClient;

View File

@ -1,5 +1,4 @@
import WebsocketAndroidClient from './WebsocketAndroidClient';
import WebsocketNativeClient from './WebsocketNativeClient';
import WebsocketWebClient from './WebsocketWebClient';
export const WebsocketState = {
@ -28,10 +27,6 @@ function getWebsocketClient(): WebsocketClient {
// If userAgent has "AndroidRobosats", we assume the app is running inside of the
// WebView of the Kotlin RoboSats Android app.
return new WebsocketAndroidClient();
} else if (window.navigator.userAgent.includes('robosats')) {
// If userAgent has "RoboSats", we assume the app is running inside of the
// react-native-web view of the RoboSats Android app.
return new WebsocketNativeClient();
} else {
// Otherwise, we assume the app is running in a web browser.
return new WebsocketWebClient();

View File

@ -1,8 +1,9 @@
import { type ApiClient, type Auth } from '..';
import { systemClient } from '../../System';
import ApiWebClient from '../ApiWebClient';
import { v4 as uuidv4 } from 'uuid';
class ApiNativeClient implements ApiClient {
class ApiAndroidClient implements ApiClient {
public useProxy = true;
private readonly webClient: ApiClient = new ApiWebClient();
@ -54,13 +55,16 @@ class ApiNativeClient implements ApiClient {
public delete: (baseUrl: string, path: string, auth?: Auth) => Promise<object | undefined> =
async (baseUrl, path, auth) => {
if (!this.useProxy) return await this.webClient.delete(baseUrl, path, auth);
return await window.NativeRobosats?.postMessage({
category: 'http',
type: 'delete',
baseUrl,
path,
headers: this.getHeaders(auth),
}).then(this.parseResponse);
const jsonHeaders = JSON.stringify(this.getHeaders(auth));
const result = await new Promise<string>((resolve, reject) => {
const uuid: string = uuidv4();
window.AndroidAppRobosats?.sendRequest(uuid, 'DELETE', baseUrl + path, jsonHeaders, '');
window.AndroidRobosats?.storePromise(uuid, resolve, reject);
});
return this.parseResponse(JSON.parse(result));
};
public post: (
@ -70,14 +74,17 @@ class ApiNativeClient implements ApiClient {
auth?: Auth,
) => Promise<object | undefined> = async (baseUrl, path, body, auth) => {
if (!this.useProxy) return await this.webClient.post(baseUrl, path, body, auth);
return await window.NativeRobosats?.postMessage({
category: 'http',
type: 'post',
baseUrl,
path,
body,
headers: this.getHeaders(auth),
}).then(this.parseResponse);
const jsonHeaders = JSON.stringify(this.getHeaders(auth));
const jsonBody = JSON.stringify(body);
const result = await new Promise<string>((resolve, reject) => {
const uuid: string = uuidv4();
window.AndroidAppRobosats?.sendRequest(uuid, 'POST', baseUrl + path, jsonHeaders, jsonBody);
window.AndroidRobosats?.storePromise(uuid, resolve, reject);
});
return this.parseResponse(JSON.parse(result));
};
public get: (baseUrl: string, path: string, auth?: Auth) => Promise<object | undefined> = async (
@ -86,14 +93,17 @@ class ApiNativeClient implements ApiClient {
auth,
) => {
if (!this.useProxy) return await this.webClient.get(baseUrl, path, auth);
return await window.NativeRobosats?.postMessage({
category: 'http',
type: 'get',
baseUrl,
path,
headers: this.getHeaders(auth),
}).then(this.parseResponse);
const jsonHeaders = JSON.stringify(this.getHeaders(auth));
const result = await new Promise<string>((resolve, reject) => {
const uuid: string = uuidv4();
window.AndroidAppRobosats?.sendRequest(uuid, 'GET', baseUrl + path, jsonHeaders, '');
window.AndroidRobosats?.storePromise(uuid, resolve, reject);
});
return this.parseResponse(JSON.parse(result));
};
}
export default ApiNativeClient;
export default ApiAndroidClient;

View File

@ -1,5 +1,5 @@
import ApiWebClient from './ApiWebClient';
import ApiNativeClient from './ApiNativeClient';
import ApiAndroidClient from './ApiAndroidClient';
export interface Auth {
tokenSHA256: string;
@ -15,5 +15,6 @@ export interface ApiClient {
delete: (baseUrl: string, path: string, auth?: Auth) => Promise<object | undefined>;
}
export const apiClient: ApiClient =
window.ReactNativeWebView != null ? new ApiNativeClient() : new ApiWebClient();
export const apiClient: ApiClient = window.navigator.userAgent.includes('AndroidRobosats')
? new ApiAndroidClient()
: new ApiWebClient();

View File

@ -56,7 +56,6 @@
"Last order #{{orderID}}": "Última ordre #{{orderID}}",
"Looking for orders!": "Buscant ordres!",
"No existing orders found": "No s'han trobat ordres",
"Recovery": "Recuperació",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "La reutilització de la identitat de trading degrada la teva privadesa davant d'altres usuaris, coordinadors i observadors.",
"Robot Garage": "Garatge de Robots",
"Store your token safely": "Guarda el teu token de manera segura",
@ -69,6 +68,7 @@
"Create a new robot and learn to use RoboSats": "Crear un nou robot i aprendre a fer servir RoboSats",
"Fast Generate Order": "Fast Generate Order",
"Recover an existing robot using your token": "Recuperar un robot existent utilitzant el teu token",
"Recovery": "Recuperació",
"Start": "Començar",
"#12": "Phrases in basic/RobotPage/index.tsx",
"Connecting to Tor": "Connectant a Tor",
@ -444,11 +444,13 @@
"You send approx {{swapSats}} LN Sats (fees might vary)": "Envies aprox. {{swapSats}} LN Sats (les taxes poden variar)",
"#46": "Phrases in components/MakerForm/SelectCoordinator.tsx",
"Disabled": "Disabled",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Loading coordinator info...": "Loading coordinator info...",
"Maker": "Creador",
"Onchain payouts enabled": "Onchain payouts enabled",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Taker": "Prenedor",
"The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!": "El proveïdor de la infraestructura LN i comunicacions. L'amfitrió serà l'encarregat de donar suport i resoldre disputes. LEs comissions de les transaccions són fixades per l'amfitrió. Assegureu-vos de seleccionar només els amfitrions en què confieu!",
"This coordinator does not support on-chain swaps.": "This coordinator does not support on-chain swaps.",
"#47": "Phrases in components/Notifications/index.tsx",
"Lightning routing failed": "L'enrutament Lightning ha fallat",
"New chat message": "Nou missatge al xat",
@ -490,16 +492,13 @@
"Accepted payment methods": "Mètodes de pagament acceptats",
"Amount of Satoshis": "Quantitat de Sats",
"Deposit": "Dipositar",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Expires in": "Expira en",
"F2F location": "Ubicació F2F",
"Loading cooridnator info...": "Loading cooridnator info...",
"Order Details": "Detalls",
"Order host": "Order host",
"Penalty lifted, good to go!": "Sanció revocada, som-hi!",
"Premium over market price": "Prima sobre el mercat",
"Price and Premium": "Preu i prima",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Swap destination": "Destí del swap",
"The order has expired": "L'ordre ha expirat",
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "La ubicació fixada és aproximada. La ubicació exacta del lloc de trobada s'ha d'intercanviar al xat xifrat.",
@ -514,10 +513,10 @@
"Claim": "Retirar",
"Enable Telegram Notifications": "Habilita notificacions a Telegram",
"Finished order": "Finished order",
"Generate with Webln": "Generar amb Webln",
"Inactive order": "Ordre inactiva",
"Invoice for {{amountSats}} Sats": "Factura per {{amountSats}} Sats",
"No active orders": "No hi ha ordres actives",
"No orders found": "No orders found",
"One active order #{{orderID}}": "Anar a ordre activa #{{orderID}}",
"Submit": "Enviar",
"Telegram enabled": "Telegram activat",
@ -545,7 +544,6 @@
"{{nickname}} is asking for a collaborative cancel": "{{nickname}} sol·licita cancel·lar col·laborativament",
"#54": "Phrases in components/TradeBox/TradeSummary.tsx",
"Buyer": "Compra",
"Completed in": "Completat en",
"Contract exchange rate": "Taxa de canvi del contracte",
"Coordinator trade revenue": "Ingressos pel coordinador de l'intercanvi",
"Export trade summary": "Exportar el resumen d'intercanvi",
@ -557,7 +555,6 @@
"Seller": "Venda",
"Sent": "Enviat",
"Taker bond": "Fiança de prenedor",
"Timestamp": "Marca d'hora",
"Trade Summary": "Resum de l'intercanvi",
"Unlocked": "Desbloquejada",
"User role": "Operació",
@ -704,7 +701,7 @@
"Rate your trade experience": "Rate your trade experience",
"Renew": "Renovar",
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats millora amb més usuaris i liquiditat. Ensenya-li RoboSats a un amic bitcoiner!",
"Sending coins to": "Enviant monedes a",
"Sending coins": "Sending coins",
"Start Again": "Començar de nou",
"Thank you for using Robosats!": "Gràcies per fer servir RoboSats!",
"Thank you! {{shortAlias}} loves you too": "Thank you! {{shortAlias}} loves you too",

View File

@ -56,7 +56,6 @@
"Last order #{{orderID}}": "Poslední objednávka #{{orderID}}",
"Looking for orders!": "Hledání objednávek!",
"No existing orders found": "Nebyly nalezeny žádné existující objednávky",
"Recovery": "Obnova",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Opětovné použití obchodní identity snižuje vaše soukromí vůči ostatním uživatelům, koordinátorům a pozorovatelům.",
"Robot Garage": "Garáž robotů",
"Store your token safely": "Uložte si svůj token bezpečně",
@ -69,6 +68,7 @@
"Create a new robot and learn to use RoboSats": "Vytvořte nového robota a naučte se používat RoboSats",
"Fast Generate Order": "Rychlé vytvoření objednávky",
"Recover an existing robot using your token": "Obnovte existujícího robota pomocí svého tokenu",
"Recovery": "Obnova",
"Start": "Start",
"#12": "Phrases in basic/RobotPage/index.tsx",
"Connecting to Tor": "Připojení k Tor",
@ -444,11 +444,13 @@
"You send approx {{swapSats}} LN Sats (fees might vary)": "Odesíláte přibližně {{swapSats}} LN Sats (poplatky se mohou lišit)",
"#46": "Phrases in components/MakerForm/SelectCoordinator.tsx",
"Disabled": "Zakázáno",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Loading coordinator info...": "Loading coordinator info...",
"Maker": "Tvůrce",
"Onchain payouts enabled": "Onchain payouts enabled",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Taker": "Příjemce",
"The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!": "The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!",
"This coordinator does not support on-chain swaps.": "Tento koordinátor nepodporuje on-chain výměny.",
"#47": "Phrases in components/Notifications/index.tsx",
"Lightning routing failed": "Lightning routing selhal",
"New chat message": "Nová zpráva v chatu",
@ -490,16 +492,13 @@
"Accepted payment methods": "Přijaté platební metody",
"Amount of Satoshis": "Částka Satoshi",
"Deposit": "Vklad",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Expires in": "Vyprší za",
"F2F location": "Místo F2F",
"Loading cooridnator info...": "Loading cooridnator info...",
"Order Details": "Detaily objednávky",
"Order host": "Hostitel objednávky",
"Penalty lifted, good to go!": "Pokuta zrušena, můžete pokračovat!",
"Premium over market price": "Přirážka oproti tržní ceně",
"Price and Premium": "Cena a přirážka",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Swap destination": "Cíl výměny",
"The order has expired": "Objednávka vypršela",
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "Připnutá poloha je přibližná. Přesné místo setkání musí být vyměněno v šifrovaném chatu.",
@ -514,10 +513,10 @@
"Claim": "Vybrat",
"Enable Telegram Notifications": "Povolit Telegram notifikace",
"Finished order": "Finished order",
"Generate with Webln": "Generovat s Webln",
"Inactive order": "Neaktivní objednávka",
"Invoice for {{amountSats}} Sats": "Faktura pro {{amountSats}} Satů",
"No active orders": "Žádné aktivní objednávky",
"No orders found": "No orders found",
"One active order #{{orderID}}": "Jedna aktivní objednávka #{{orderID}}",
"Submit": "Odeslat",
"Telegram enabled": "Telegram povolen",
@ -545,7 +544,6 @@
"{{nickname}} is asking for a collaborative cancel": "{{nickname}} žádá o spolupracivé zrušení obchodu",
"#54": "Phrases in components/TradeBox/TradeSummary.tsx",
"Buyer": "Kupující",
"Completed in": "Dokončeno za",
"Contract exchange rate": "Smluvní kurz",
"Coordinator trade revenue": "Příjmy z obchodu koordinátora",
"Export trade summary": "Exportovat shrnutí obchodu",
@ -557,7 +555,6 @@
"Seller": "Prodávající",
"Sent": "Odesláno",
"Taker bond": "Kauce příjemce",
"Timestamp": "Časové razítko",
"Trade Summary": "Shrnutí obchodu",
"Unlocked": "Odemknuto",
"User role": "Uživatelská role",
@ -704,7 +701,7 @@
"Rate your trade experience": "Ohodnoť svou obchodní zkušenost",
"Renew": "Obnovit",
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats se zlepšuje s větší likviditou a uživateli. Řekni svým přátelům bitcoinistům o Robosats!",
"Sending coins to": "Odesílání mincí na",
"Sending coins": "Sending coins",
"Start Again": "Začít znovu",
"Thank you for using Robosats!": "Děkujeme, že používáš Robosats!",
"Thank you! {{shortAlias}} loves you too": "Děkujeme! Také tě miluje {{shortAlias}}",

View File

@ -56,7 +56,6 @@
"Last order #{{orderID}}": "Letzte Bestellung #{{orderID}}",
"Looking for orders!": "Suche nach Bestellungen!",
"No existing orders found": "Keine bestehenden Bestellungen gefunden",
"Recovery": "Wiederherstellung",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Das Wiederverwenden deiner Handelsidentität verringert deine Privatsphäre gegenüber anderen Benutzern, Koordinatoren und Beobachtern.",
"Robot Garage": "Roboter-Garage",
"Store your token safely": "Verwahre dein Token sicher",
@ -69,6 +68,7 @@
"Create a new robot and learn to use RoboSats": "Erstelle einen neuen Roboter und lerne RoboSats zu benutzen",
"Fast Generate Order": "Schnelle Auftragserstellung",
"Recover an existing robot using your token": "Stelle einen bestehenden Roboter mit deinem Token wieder her",
"Recovery": "Wiederherstellung",
"Start": "Start",
"#12": "Phrases in basic/RobotPage/index.tsx",
"Connecting to Tor": "Verbinden mit Tor",
@ -444,11 +444,13 @@
"You send approx {{swapSats}} LN Sats (fees might vary)": "Du sendest ungefähr {{swapSats}} LN Sats (Gebühren können variieren)",
"#46": "Phrases in components/MakerForm/SelectCoordinator.tsx",
"Disabled": "Deaktiviert",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Loading coordinator info...": "Loading coordinator info...",
"Maker": "Ersteller",
"Onchain payouts enabled": "Onchain-Auszahlungen aktiviert",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Taker": "Nehmer",
"The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!": "Der Anbieter der Lightning- und Kommunikationsinfrastruktur. Der Host wird für die Bereitstellung von Support und die Lösung von Streitfällen verantwortlich sein. Die Handelsgebühren werden vom Host festgelegt. Stellen Sie sicher, dass Sie nur Hosts auswählen, denen Sie vertrauen!",
"This coordinator does not support on-chain swaps.": "Dieser Koordinator unterstützt keine On-Chain-Swaps.",
"#47": "Phrases in components/Notifications/index.tsx",
"Lightning routing failed": "Lightning-Routing fehlgeschlagen",
"New chat message": "Neue Chat-Nachricht",
@ -490,16 +492,13 @@
"Accepted payment methods": "Akzeptierte Zahlungsmethoden",
"Amount of Satoshis": "Anzahl der Satoshis",
"Deposit": "Einzahlungstimer",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Expires in": "Läuft ab in",
"F2F location": "F2F lokale",
"Loading cooridnator info...": "Loading cooridnator info...",
"Order Details": "Bestelldetails",
"Order host": "Bestellhost",
"Penalty lifted, good to go!": "Strafe aufgehoben, es kann losgehen!",
"Premium over market price": "Aufschlag über dem Marktpreis",
"Price and Premium": "Preis und Aufpreis",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Swap destination": "Swap-Ziel",
"The order has expired": "Die Bestellung ist abgelaufen",
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "Die angeheftete Position ist ungefähr. Der genaue Standort für den Treffpunkt muss im verschlüsselten Chat ausgetauscht werden.",
@ -514,10 +513,10 @@
"Claim": "Erhalten",
"Enable Telegram Notifications": "Telegram-Benachrichtigungen aktivieren",
"Finished order": "Finished order",
"Generate with Webln": "Mit Webln generieren",
"Inactive order": "Inaktive Bestellung",
"Invoice for {{amountSats}} Sats": "Rechnung für {{amountSats}} Sats",
"No active orders": "Keine aktive Bestellung",
"No orders found": "No orders found",
"One active order #{{orderID}}": "Eine aktive Bestellung #{{orderID}}",
"Submit": "Bestätigen",
"Telegram enabled": "Telegram aktiviert",
@ -545,7 +544,6 @@
"{{nickname}} is asking for a collaborative cancel": "{{nickname}} bittet um gemeinsamen Abbruch",
"#54": "Phrases in components/TradeBox/TradeSummary.tsx",
"Buyer": "Käufer",
"Completed in": "Abgeschlossen in",
"Contract exchange rate": "Vertraglicher Wechselkurs",
"Coordinator trade revenue": "Koordinator-Handelserlös",
"Export trade summary": "Handelsübersicht exportieren",
@ -557,7 +555,6 @@
"Seller": "Verkäufer",
"Sent": "Gesendet",
"Taker bond": "Nehmer-Kaution",
"Timestamp": "Zeitstempel",
"Trade Summary": "Handelsübersicht",
"Unlocked": "Entsperrt",
"User role": "Benutzerrolle",
@ -704,7 +701,7 @@
"Rate your trade experience": "Bewerte deine Handelserfahrung",
"Renew": "Erneuern",
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats wird noch besser mit mehr Nutzern und Liquidität. Erzähl einem Bitcoin-Freund von uns!",
"Sending coins to": "Sende Münzen an",
"Sending coins": "Sending coins",
"Start Again": "Nochmal",
"Thank you for using Robosats!": "Danke, dass du Robosats benutzt hast!",
"Thank you! {{shortAlias}} loves you too": "Danke! {{shortAlias}} liebt dich auch",

View File

@ -56,7 +56,6 @@
"Last order #{{orderID}}": "Last order #{{orderID}}",
"Looking for orders!": "Looking for orders!",
"No existing orders found": "No existing orders found",
"Recovery": "Recovery",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Reusing trading identity degrades your privacy against other users, coordinators and observers.",
"Robot Garage": "Robot Garage",
"Store your token safely": "Store your token safely",
@ -69,6 +68,7 @@
"Create a new robot and learn to use RoboSats": "Create a new robot and learn to use RoboSats",
"Fast Generate Order": "Fast Generate Order",
"Recover an existing robot using your token": "Recover an existing robot using your token",
"Recovery": "Recovery",
"Start": "Start",
"#12": "Phrases in basic/RobotPage/index.tsx",
"Connecting to Tor": "Connecting to Tor",
@ -444,11 +444,13 @@
"You send approx {{swapSats}} LN Sats (fees might vary)": "You send approx {{swapSats}} LN Sats (fees might vary)",
"#46": "Phrases in components/MakerForm/SelectCoordinator.tsx",
"Disabled": "Disabled",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Loading coordinator info...": "Loading coordinator info...",
"Maker": "Maker",
"Onchain payouts enabled": "Onchain payouts enabled",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Taker": "Taker",
"The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!": "The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!",
"This coordinator does not support on-chain swaps.": "This coordinator does not support on-chain swaps.",
"#47": "Phrases in components/Notifications/index.tsx",
"Lightning routing failed": "Lightning routing failed",
"New chat message": "New chat message",
@ -490,16 +492,13 @@
"Accepted payment methods": "Accepted payment methods",
"Amount of Satoshis": "Amount of Satoshis",
"Deposit": "Deposit",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Expires in": "Expires in",
"F2F location": "F2F location",
"Loading cooridnator info...": "Loading cooridnator info...",
"Order Details": "Order Details",
"Order host": "Order host",
"Penalty lifted, good to go!": "Penalty lifted, good to go!",
"Premium over market price": "Premium over market price",
"Price and Premium": "Price and Premium",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Swap destination": "Swap destination",
"The order has expired": "The order has expired",
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
@ -514,10 +513,10 @@
"Claim": "Claim",
"Enable Telegram Notifications": "Enable Telegram Notifications",
"Finished order": "Finished order",
"Generate with Webln": "Generate with Webln",
"Inactive order": "Inactive order",
"Invoice for {{amountSats}} Sats": "Invoice for {{amountSats}} Sats",
"No active orders": "No active orders",
"No orders found": "No orders found",
"One active order #{{orderID}}": "One active order #{{orderID}}",
"Submit": "Submit",
"Telegram enabled": "Telegram enabled",
@ -545,7 +544,6 @@
"{{nickname}} is asking for a collaborative cancel": "{{nickname}} is asking for a collaborative cancel",
"#54": "Phrases in components/TradeBox/TradeSummary.tsx",
"Buyer": "Buyer",
"Completed in": "Completed in",
"Contract exchange rate": "Contract exchange rate",
"Coordinator trade revenue": "Coordinator trade revenue",
"Export trade summary": "Export trade summary",
@ -557,7 +555,6 @@
"Seller": "Seller",
"Sent": "Sent",
"Taker bond": "Taker bond",
"Timestamp": "Timestamp",
"Trade Summary": "Trade Summary",
"Unlocked": "Unlocked",
"User role": "User role",
@ -704,7 +701,7 @@
"Rate your trade experience": "Rate your trade experience",
"Renew": "Renew",
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!",
"Sending coins to": "Sending coins to",
"Sending coins": "Sending coins",
"Start Again": "Start Again",
"Thank you for using Robosats!": "Thank you for using Robosats!",
"Thank you! {{shortAlias}} loves you too": "Thank you! {{shortAlias}} loves you too",

View File

@ -56,7 +56,6 @@
"Last order #{{orderID}}": "Última orden #{{orderID}}",
"Looking for orders!": "Buscando órdenes!",
"No existing orders found": "No se encontraron órdenes existentes",
"Recovery": "Recuperar",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Reusing trading identity degrades your privacy against other users, coordinators and observers.",
"Robot Garage": "Garaje de robots",
"Store your token safely": "Guarda tu token de forma segura",
@ -69,6 +68,7 @@
"Create a new robot and learn to use RoboSats": "Crea un nuevo robot y aprende a usar RoboSats",
"Fast Generate Order": "Genera una Orden al instante",
"Recover an existing robot using your token": "Recupera un robot existente usando tu token",
"Recovery": "Recuperar",
"Start": "Empezar",
"#12": "Phrases in basic/RobotPage/index.tsx",
"Connecting to Tor": "Conectando con Tor",
@ -444,11 +444,13 @@
"You send approx {{swapSats}} LN Sats (fees might vary)": "Envías aproximadamente {{swapSats}} LN Sats (las comisiones pueden variar)",
"#46": "Phrases in components/MakerForm/SelectCoordinator.tsx",
"Disabled": "Desactivado",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Loading coordinator info...": "Loading coordinator info...",
"Maker": "Creador",
"Onchain payouts enabled": "Pagos onchain habilitados",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Taker": "Tomador",
"The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!": "El proveedor de la infraestructura de comunicación y lightning. El anfitrión estará a cargo de proporcionar soporte y resolver disputas. Las comisiones comerciales son establecidas por el anfitrión. ¡Asegúrate de seleccionar solo anfitriones de órdenes en los que confíes!",
"This coordinator does not support on-chain swaps.": "Este coordinador no soporta intercambios on-chain.",
"#47": "Phrases in components/Notifications/index.tsx",
"Lightning routing failed": "Enrutado Lightning fallido",
"New chat message": "Nuevo mensaje en el chat",
@ -490,16 +492,13 @@
"Accepted payment methods": "Métodos de pago aceptados",
"Amount of Satoshis": "Cantidad de Sats",
"Deposit": "Depósito",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Expires in": "Expira en",
"F2F location": "Ubicación cara a cara",
"Loading cooridnator info...": "Loading cooridnator info...",
"Order Details": "Detalles de la Orden",
"Order host": "Anfitrión de la Orden",
"Penalty lifted, good to go!": "Sanción revocada, ¡Todo listo!",
"Premium over market price": "Prima sobre el precio de mercado",
"Price and Premium": "Precio y Prima",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Swap destination": "Destino del Swap",
"The order has expired": "La orden ha expirado",
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "La ubicación marcada es aproximada. La ubicación exacta para el punto de encuentro debe ser intercambiada en el chat cifrado.",
@ -514,10 +513,10 @@
"Claim": "Reclamar",
"Enable Telegram Notifications": "Activar Notificaciones de Telegram",
"Finished order": "Finished order",
"Generate with Webln": "Generar con Webln",
"Inactive order": "Orden inactiva",
"Invoice for {{amountSats}} Sats": "Factura de {{amountSats}} Sats",
"No active orders": "No hay órdenes activas",
"No orders found": "No orders found",
"One active order #{{orderID}}": "Una orden activa #{{orderID}}",
"Submit": "Enviar",
"Telegram enabled": "Telegram activado",
@ -545,7 +544,6 @@
"{{nickname}} is asking for a collaborative cancel": "{{nickname}} solicita la cancelación colaborativa",
"#54": "Phrases in components/TradeBox/TradeSummary.tsx",
"Buyer": "Comprador",
"Completed in": "Completado en",
"Contract exchange rate": "Tasa de cambio del contrato",
"Coordinator trade revenue": "Ingresos del intercambio del coordinador",
"Export trade summary": "Exportar el resumen de la transacción",
@ -557,7 +555,6 @@
"Seller": "Vendedor",
"Sent": "Enviado",
"Taker bond": "Fianza del tomador",
"Timestamp": "Marca de tiempo",
"Trade Summary": "Resumen del intercambio",
"Unlocked": "Desbloqueado",
"User role": "Rol del usuario",
@ -704,7 +701,7 @@
"Rate your trade experience": "Califica tu experiencia de intercambio",
"Renew": "Renovar",
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats mejora con más liquidez y usuarios. ¡Háblale a un amigo bitcoiner sobre RoboSats!",
"Sending coins to": "Enviando monedas a",
"Sending coins": "Sending coins",
"Start Again": "Empezar de nuevo",
"Thank you for using Robosats!": "¡Gracias por usar RoboSats!",
"Thank you! {{shortAlias}} loves you too": "¡Gracias! {{shortAlias}} también te quiere",

View File

@ -56,7 +56,6 @@
"Last order #{{orderID}}": "Azken eskaera #{{orderID}}",
"Looking for orders!": "Eskaerak bilatzen!",
"No existing orders found": "Ez dauden eskaerarik aurkitu",
"Recovery": "Berreskuratzea",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Truke nortasuna berriro erabiltzeak pribatutasuna kaltetzen du beste erabiltzaile, koordinatzaile eta behatzaileei dagokienez.",
"Robot Garage": "Robot Garajea",
"Store your token safely": "Gorde zure tokena era seguru batean",
@ -69,6 +68,7 @@
"Create a new robot and learn to use RoboSats": "Sortu robot berria eta ikasi RoboSats erabiltzen",
"Fast Generate Order": "Sortu Eskaera Azkar",
"Recover an existing robot using your token": "Berreskuratu dauden robota zure tokenaren bidez",
"Recovery": "Berreskuratzea",
"Start": "Hasi",
"#12": "Phrases in basic/RobotPage/index.tsx",
"Connecting to Tor": "Torera konektatzen",
@ -444,11 +444,13 @@
"You send approx {{swapSats}} LN Sats (fees might vary)": "Gutxi gorabehera bidaltzen dituzu {{swapSats}} LN Sats (komisioak aldatu daitezke)",
"#46": "Phrases in components/MakerForm/SelectCoordinator.tsx",
"Disabled": "Desgaituta",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Loading coordinator info...": "Loading coordinator info...",
"Maker": "Egile",
"Onchain payouts enabled": "Onchain ordainketak gaituta",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Taker": "Hartzaile",
"The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!": "Argiaren eta komunikazio azpiegituren hornitzailea. Ostalariak laguntza emateaz eta eztabaidak konpontzeaz arduratuko da. Truke kuotak ostalariak bideratzen ditu. Ziurtatu konfiantza duzun ostalari eskaerak aukeratzea!",
"This coordinator does not support on-chain swaps.": "Koordinatzaile honek ez ditu onartzen katea gaineko trukeak.",
"#47": "Phrases in components/Notifications/index.tsx",
"Lightning routing failed": "Txinparta bideratzea huts egin du",
"New chat message": "Mezu berria",
@ -490,16 +492,13 @@
"Accepted payment methods": "Onartuta ordainketa moduak",
"Amount of Satoshis": "Satoshi kopurua",
"Deposit": "Gordailu tenporizadorea",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Expires in": "Iraungitze denbora",
"F2F location": "Aurre Aurre lokazioa",
"Loading cooridnator info...": "Loading cooridnator info...",
"Order Details": "Eskaeraren Xehetasunak",
"Order host": "Eskaera ostalaria",
"Penalty lifted, good to go!": "Zigorra kendu da, prest!",
"Premium over market price": "Merkatuko prezioarekiko prima",
"Price and Premium": "Prezioa eta Prima",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Swap destination": "Trukearen norakoa",
"The order has expired": "Eskera iraungi da",
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "Jarritako kokapena gutxi gorabeherakoa da. Topagunerako kokapen zehatza txat enkriptatuaren bidez trukatu behar da.",
@ -514,10 +513,10 @@
"Claim": "Eskatu",
"Enable Telegram Notifications": "Baimendu Telegram Jakinarazpenak",
"Finished order": "Finished order",
"Generate with Webln": "Sortu Webln Laketa",
"Inactive order": "Eskaera ez aktiboa",
"Invoice for {{amountSats}} Sats": "{{amountSats}} Sateko faktura",
"No active orders": "Ez dago eskaera aktiboak",
"No orders found": "No orders found",
"One active order #{{orderID}}": "Eskaera aktiboa #{{orderID}}",
"Submit": "Bidali",
"Telegram enabled": "Telegram baimendua",
@ -545,7 +544,6 @@
"{{nickname}} is asking for a collaborative cancel": "{{nickname}} lankidetzaz ezeztatzea eskatu du",
"#54": "Phrases in components/TradeBox/TradeSummary.tsx",
"Buyer": "Erosle",
"Completed in": "Igarotako denbora",
"Contract exchange rate": "Kontratuaren truke-tasa",
"Coordinator trade revenue": "Koordinatzailearen salerosketa etekina",
"Export trade summary": "Esportatu salerosketa laburpena",
@ -557,7 +555,6 @@
"Seller": "Saltzaile",
"Sent": "Bidalitakoa",
"Taker bond": "Hartzaile fidantza",
"Timestamp": "Amaiera ordua",
"Trade Summary": "Salerosketaren laburpena",
"Unlocked": "Askatu da",
"User role": "Erabiltzaile rola",
@ -704,7 +701,7 @@
"Rate your trade experience": "Baloratu zure truke esperientzia",
"Renew": "Berritu",
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats hobetu egiten da likidezia eta erabiltzaile gehiagorekin. Aipatu Robosats lagun bitcoinzale bati!",
"Sending coins to": "Txanponak bidaltzen",
"Sending coins": "Sending coins",
"Start Again": "Berriz Hasi",
"Thank you for using Robosats!": "Mila esker Robosats erabiltzeagatik!",
"Thank you! {{shortAlias}} loves you too": "Eskerrik asko! {{shortAlias}} ere maite zaitu",

View File

@ -56,7 +56,6 @@
"Last order #{{orderID}}": "Dernier ordre #{{orderID}}",
"Looking for orders!": "Recherche de commandes !",
"No existing orders found": "Aucune commande existante trouvée",
"Recovery": "Récupération",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "La réutilisation de l'identité d'échange dégrade votre vie privée par rapport aux autres utilisateurs, coordinateurs et observateurs.",
"Robot Garage": "Garage des robots",
"Store your token safely": "Stockez votre jeton en sécurité",
@ -69,6 +68,7 @@
"Create a new robot and learn to use RoboSats": "Créer un nouveau robot et apprendre à utiliser RoboSats",
"Fast Generate Order": "Générer une commande rapidement",
"Recover an existing robot using your token": "Récupérer un robot existant à l'aide de votre jeton",
"Recovery": "Récupération",
"Start": "Commencer",
"#12": "Phrases in basic/RobotPage/index.tsx",
"Connecting to Tor": "Connexion au réseau Tor",
@ -444,11 +444,13 @@
"You send approx {{swapSats}} LN Sats (fees might vary)": "Vous envoyez environ {{swapSats}} LN Sats (les frais peuvent varier)",
"#46": "Phrases in components/MakerForm/SelectCoordinator.tsx",
"Disabled": "Désactivé",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Loading coordinator info...": "Loading coordinator info...",
"Maker": "Créateur",
"Onchain payouts enabled": "Paiements onchain activés",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Taker": "Preneur",
"The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!": "Le fournisseur de l'infrastructure lightning et de communication. L'hôte sera chargé de fournir un support et de résoudre les litiges. Les frais de transaction sont fixés par l'hôte. Assurez-vous de ne sélectionner que des hôtes de commande en qui vous avez confiance !",
"This coordinator does not support on-chain swaps.": "Ce coordinateur ne prend pas en charge les échanges on-chain.",
"#47": "Phrases in components/Notifications/index.tsx",
"Lightning routing failed": "Échec du routage Lightning",
"New chat message": "Nouveau message de chat",
@ -490,16 +492,13 @@
"Accepted payment methods": "Modes de paiement acceptés",
"Amount of Satoshis": "Montant de Satoshis",
"Deposit": "Dépôt",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Expires in": "Expire dans",
"F2F location": "Emplacement F2F",
"Loading cooridnator info...": "Loading cooridnator info...",
"Order Details": "Détails de l'ordre",
"Order host": "Hôte de l'ordre",
"Penalty lifted, good to go!": "Pénalité levée, vous pouvez y aller!",
"Premium over market price": "Prime sur le prix du marché",
"Price and Premium": "Prix et prime",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Swap destination": "Destination de l'échange",
"The order has expired": "L'ordre a expiré",
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "L'emplacement épinglé est approximatif. L'emplacement exact du lieu de rencontre doit être échangé dans le chat crypté.",
@ -514,10 +513,10 @@
"Claim": "Réclamer",
"Enable Telegram Notifications": "Activer les notifications Telegram",
"Finished order": "Finished order",
"Generate with Webln": "Générer avec Webln",
"Inactive order": "Ordre inactif",
"Invoice for {{amountSats}} Sats": "Facture pour {{amountSats}} Sats",
"No active orders": "Aucun ordre actif",
"No orders found": "No orders found",
"One active order #{{orderID}}": "Un ordre actif #{{orderID}}",
"Submit": "Soumettre",
"Telegram enabled": "Telegram activé",
@ -545,7 +544,6 @@
"{{nickname}} is asking for a collaborative cancel": "{{nickname}} demande une annulation collaborative",
"#54": "Phrases in components/TradeBox/TradeSummary.tsx",
"Buyer": "Acheteur",
"Completed in": "Terminé en",
"Contract exchange rate": "Taux de change du contrat",
"Coordinator trade revenue": "Revenu de la transaction du coordinateur",
"Export trade summary": "Exporter le résumé de la transaction",
@ -557,7 +555,6 @@
"Seller": "Vendeur",
"Sent": "Envoyé",
"Taker bond": "Caution du preneur",
"Timestamp": "Horodatage",
"Trade Summary": "Résumé de la transaction",
"Unlocked": "Débloqué",
"User role": "Rôle utilisateur",
@ -704,7 +701,7 @@
"Rate your trade experience": "Évaluez votre expérience de trading",
"Renew": "Renouveler",
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats s'améliore avec plus de liquidité et d'utilisateurs. Parlez de Robosats à un ami bitcoiner!",
"Sending coins to": "Envoyer des pièces à",
"Sending coins": "Sending coins",
"Start Again": "Recommencer",
"Thank you for using Robosats!": "Merci d'utiliser Robosats!",
"Thank you! {{shortAlias}} loves you too": "Merci ! {{shortAlias}} vous aime aussi",

View File

@ -56,7 +56,6 @@
"Last order #{{orderID}}": "Ultimo ordine #{{orderID}}",
"Looking for orders!": "Alla ricerca di ordini!",
"No existing orders found": "Nessun ordine esistente trovato",
"Recovery": "Recupero",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Il riutilizzo di identità per gli scambi riduce la privacy rispetto ad altri utenti, coordinatori e osservatori.",
"Robot Garage": "Garage del robot",
"Store your token safely": "Custodisci il tuo token in modo sicuro",
@ -69,6 +68,7 @@
"Create a new robot and learn to use RoboSats": "Crea un nuovo robot e impara ad utilizzare RoboSats",
"Fast Generate Order": "Genera ordine rapido",
"Recover an existing robot using your token": "Recupera un robot esistente con il tuo token",
"Recovery": "Recupero",
"Start": "Inizia",
"#12": "Phrases in basic/RobotPage/index.tsx",
"Connecting to Tor": "Connessione a Tor",
@ -444,11 +444,13 @@
"You send approx {{swapSats}} LN Sats (fees might vary)": "Invii circa {{swapSats}} LN Sats (le commissioni possono variare)",
"#46": "Phrases in components/MakerForm/SelectCoordinator.tsx",
"Disabled": "Disabilitato",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Loading coordinator info...": "Loading coordinator info...",
"Maker": "Creatore",
"Onchain payouts enabled": "Pagamento onchain abilitato",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Taker": "Acquirente",
"The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!": "Il fornitore dell'infrastruttura lightning e comunicazione. L'host sarà responsabile del supporto e della risoluzione delle dispute. Le commissioni commerciali sono fissate dall'host. Assicurati di selezionare solo gli host degli ordini di cui ti fidi!",
"This coordinator does not support on-chain swaps.": "Questo coordinatore non supporta gli swap on-chain.",
"#47": "Phrases in components/Notifications/index.tsx",
"Lightning routing failed": "Instradamento Lightning fallito",
"New chat message": "Nuovo messaggio di chat",
@ -490,16 +492,13 @@
"Accepted payment methods": "Metodi di pagamento accettati",
"Amount of Satoshis": "Importo di Satoshi",
"Deposit": "Deposito",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Expires in": "Scade in",
"F2F location": "Posizione F2F",
"Loading cooridnator info...": "Loading cooridnator info...",
"Order Details": "Dettagli Ordine",
"Order host": "Host dell'ordine",
"Penalty lifted, good to go!": "Penalità revocata, puoi procedere!",
"Premium over market price": "Prezzo maggiore del prezzo di mercato",
"Price and Premium": "Prezzo e Premio",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Swap destination": "Destinazione swap",
"The order has expired": "L'ordine è scaduto",
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "La posizione impiantata è approssimativa. La posizione esatta per il luogo di incontro deve essere scambiata nella chat crittografata.",
@ -514,10 +513,10 @@
"Claim": "Richiedi",
"Enable Telegram Notifications": "Abilita Notifiche Telegram",
"Finished order": "Finished order",
"Generate with Webln": "Genera con Webln",
"Inactive order": "Ordine inattivo",
"Invoice for {{amountSats}} Sats": "Fattura per {{amountSats}} Sats",
"No active orders": "Nessun ordine attivo",
"No orders found": "No orders found",
"One active order #{{orderID}}": "Un ordine attivo #{{orderID}}",
"Submit": "Invia",
"Telegram enabled": "Telegram abilitato",
@ -545,7 +544,6 @@
"{{nickname}} is asking for a collaborative cancel": "{{nickname}} ha richiesto un annullamento collaborativo",
"#54": "Phrases in components/TradeBox/TradeSummary.tsx",
"Buyer": "Acquirente",
"Completed in": "Completato in",
"Contract exchange rate": "Tasso di cambio contrattuale",
"Coordinator trade revenue": "Guadagno del coordinatore",
"Export trade summary": "Esporta riepilogo dello scambio",
@ -557,7 +555,6 @@
"Seller": "Venditore",
"Sent": "Inviato",
"Taker bond": "Cauzione dell'acquirente",
"Timestamp": "Timestamp",
"Trade Summary": "Riepilogo scambio",
"Unlocked": "Sbloccato",
"User role": "Ruolo utente",
@ -704,7 +701,7 @@
"Rate your trade experience": "Valuta la tua esperienza di scambio",
"Renew": "Rinnova",
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats migliora con più liquidità e utenti. Parla di RoboSats a un amico bitcoiner!",
"Sending coins to": "Invio monete a",
"Sending coins": "Sending coins",
"Start Again": "Ricominciamo",
"Thank you for using Robosats!": "Grazie per aver usato Robosats!",
"Thank you! {{shortAlias}} loves you too": "Grazie! Anche {{shortAlias}} ti ama",

View File

@ -56,7 +56,6 @@
"Last order #{{orderID}}": "直前の注文 #{{orderID}}",
"Looking for orders!": "注文を探しています!",
"No existing orders found": "既存の注文は見つかりません",
"Recovery": "復元",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "同一の身元を再利用することは、他のユーザー、管理者、観察者に対してプライバシーを弱める可能性があります。",
"Robot Garage": "ロボットガレージ",
"Store your token safely": "安全にトークンを保存してください。",
@ -69,6 +68,7 @@
"Create a new robot and learn to use RoboSats": "新しいロボットを構築して、RoboSatsの使い方を学びます",
"Fast Generate Order": "注文をすばやく生成",
"Recover an existing robot using your token": "トークンを使用して既存のロボットを復元します",
"Recovery": "復元",
"Start": "開始",
"#12": "Phrases in basic/RobotPage/index.tsx",
"Connecting to Tor": "Torネットワークに接続中",
@ -444,11 +444,13 @@
"You send approx {{swapSats}} LN Sats (fees might vary)": "約{{swapSats}} ライトニングSatsを送信します手数料は異なる場合があります",
"#46": "Phrases in components/MakerForm/SelectCoordinator.tsx",
"Disabled": "無効",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Loading coordinator info...": "Loading coordinator info...",
"Maker": "メーカー",
"Onchain payouts enabled": "オンチェーンでの支払いが有効",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Taker": "テイカー",
"The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!": "ライトニングと通信インフラを提供します。ホストはサポートの提供と論争の解決を担当します。取引手数料はホストによって設定されます。信頼できる注文ホストのみを選択するようにしてください!",
"This coordinator does not support on-chain swaps.": "このコーディネーターは、オンチェーンスワップをサポートしていません。",
"#47": "Phrases in components/Notifications/index.tsx",
"Lightning routing failed": "ライトニングのルーティングに失敗しました",
"New chat message": "新しいチャットメッセージ",
@ -490,16 +492,13 @@
"Accepted payment methods": "受け付ける支払い方法",
"Amount of Satoshis": "サトシの金額",
"Deposit": "デポジットタイマー",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Expires in": "有効期限",
"F2F location": "対面の位置",
"Loading cooridnator info...": "Loading cooridnator info...",
"Order Details": "注文の詳細",
"Order host": "注文ホスト",
"Penalty lifted, good to go!": "ペナルティが解除されました、準備万端!",
"Premium over market price": "市場価格に対するプレミアム",
"Price and Premium": "価格とプレミアム",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Swap destination": "スワップの宛先",
"The order has expired": "注文は期限切れになりました",
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "ピンされた場所は近似であり、会合場所の正確な位置は暗号化されたチャットを介して交換する必要があります。",
@ -514,10 +513,10 @@
"Claim": "請求する",
"Enable Telegram Notifications": "Telegram通知を有効にする",
"Finished order": "Finished order",
"Generate with Webln": "Weblnで生成",
"Inactive order": "非アクティブなオーダー",
"Invoice for {{amountSats}} Sats": " {{amountSats}} Satsのインボイス",
"No active orders": "アクティブなオーダーはありません",
"No orders found": "No orders found",
"One active order #{{orderID}}": "アクティブなオーダー #{{orderID}} に進む",
"Submit": "送信",
"Telegram enabled": "Telegramが有効になりました",
@ -545,7 +544,6 @@
"{{nickname}} is asking for a collaborative cancel": "{{nickname}}が共同キャンセルを求めています",
"#54": "Phrases in components/TradeBox/TradeSummary.tsx",
"Buyer": "買い手",
"Completed in": "完了",
"Contract exchange rate": "契約為替レート",
"Coordinator trade revenue": "取引コーディネーターの収益",
"Export trade summary": "取引概要をエクスポートする",
@ -557,7 +555,6 @@
"Seller": "売り手",
"Sent": "送信",
"Taker bond": "テイカーの担保金",
"Timestamp": "タイムスタンプ",
"Trade Summary": "取引概要",
"Unlocked": "ロック解除済み",
"User role": "ユーザーの役割",
@ -704,7 +701,7 @@
"Rate your trade experience": "取引体験を評価する",
"Renew": "更新する",
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSatsはより多くの流動性とユーザーでより良くなります。ビットコイナーのお友達にRoboSatsについて話してください",
"Sending coins to": "コインを送信する先",
"Sending coins": "Sending coins",
"Start Again": "最初からやり直す",
"Thank you for using Robosats!": "Robosatsをご利用いただきありがとうございます",
"Thank you! {{shortAlias}} loves you too": "ありがとうございます!{{shortAlias}}もあなたを愛しています",

View File

@ -56,7 +56,6 @@
"Last order #{{orderID}}": "Ostatnie zamówienie #{{orderID}}",
"Looking for orders!": "Szukam zamówień!",
"No existing orders found": "Nie znaleziono istniejących zamówień",
"Recovery": "Odzyskiwanie",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Ponowne użycie tożsamości handlowej pogarsza twoją prywatność w stosunku do innych użytkowników, koordynatorów i obserwatorów.",
"Robot Garage": "Garaż robota",
"Store your token safely": "Przechowuj swój token bezpiecznie",
@ -69,6 +68,7 @@
"Create a new robot and learn to use RoboSats": "Utwórz nowego robota i naucz się używać RoboSats",
"Fast Generate Order": "Szybkie generowanie zamówień",
"Recover an existing robot using your token": "Odzyskaj istniejącego robota za pomocą swojego tokenu",
"Recovery": "Odzyskiwanie",
"Start": "Start",
"#12": "Phrases in basic/RobotPage/index.tsx",
"Connecting to Tor": "Łączenie z Tor",
@ -444,11 +444,13 @@
"You send approx {{swapSats}} LN Sats (fees might vary)": "Wysyłasz około {{swapSats}} LN Sats (opłaty mogą się różnić)",
"#46": "Phrases in components/MakerForm/SelectCoordinator.tsx",
"Disabled": "Wyłączone",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Loading coordinator info...": "Loading coordinator info...",
"Maker": "Twórca",
"Onchain payouts enabled": "Wypłaty onchain włączone",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Taker": "Nabywca",
"The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!": "Dostawca infrastruktury lightning i komunikacji. Host będzie odpowiedzialny za udzielanie wsparcia i rozwiązywanie sporów. Opłaty za handel są ustalane przez hosta. Upewnij się, że wybierasz tylko hostów zamówień, którym ufasz!",
"This coordinator does not support on-chain swaps.": "Ten koordynator nie obsługuje wymiany on-chain.",
"#47": "Phrases in components/Notifications/index.tsx",
"Lightning routing failed": "Routing Lightning nie powiódł się",
"New chat message": "Nowa wiadomość czat",
@ -490,16 +492,13 @@
"Accepted payment methods": "Akceptowane metody płatności",
"Amount of Satoshis": "Ilość Satoshis",
"Deposit": "Depozyt",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Expires in": "Wygasa za",
"F2F location": "Lokalizacja F2F",
"Loading cooridnator info...": "Loading cooridnator info...",
"Order Details": "Szczegóły zamówienia",
"Order host": "Host zamówienia",
"Penalty lifted, good to go!": "Kara zniesiona, można działać!",
"Premium over market price": "Premia ponad cenę rynkową",
"Price and Premium": "Cena i Premia",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Swap destination": "Miejsce docelowe wymiany",
"The order has expired": "Zamówienie wygasło",
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "Przypięta lokalizacja jest przybliżona. Dokładna lokalizacja miejsca spotkania musi być wymieniona w zaszyfrowanym czacie.",
@ -514,10 +513,10 @@
"Claim": "Odebrać",
"Enable Telegram Notifications": "Włącz powiadomienia Telegram",
"Finished order": "Finished order",
"Generate with Webln": "Generuj z Webln",
"Inactive order": "Nieaktywne zamówienie",
"Invoice for {{amountSats}} Sats": "Faktura za {{amountSats}} Sats",
"No active orders": "Brak aktywnych zamówień",
"No orders found": "No orders found",
"One active order #{{orderID}}": "Jedno aktywne zamówienie #{{orderID}}",
"Submit": "Prześlij",
"Telegram enabled": "Telegram włączony",
@ -545,7 +544,6 @@
"{{nickname}} is asking for a collaborative cancel": "{{nickname}} prosi o wspólne anulowanie",
"#54": "Phrases in components/TradeBox/TradeSummary.tsx",
"Buyer": "Kupujący",
"Completed in": "Ukończone w",
"Contract exchange rate": "Kurs wymiany kontraktowej",
"Coordinator trade revenue": "Przychody z handlu koordynatora",
"Export trade summary": "Eksportuj podsumowanie handlu",
@ -557,7 +555,6 @@
"Seller": "Sprzedawca",
"Sent": "Wysłane",
"Taker bond": "Obligacja nabywcy",
"Timestamp": "Znacznik czasu",
"Trade Summary": "Podsumowanie Handlu",
"Unlocked": "Odblokowane",
"User role": "Rola użytkownika",
@ -704,7 +701,7 @@
"Rate your trade experience": "Oceń swoje doświadczenia handlowe",
"Renew": "Odnów",
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats staje się lepszy dzięki większej płynności i użytkownikom. Powiedz znajomemu bitcoinerowi o Robosats!",
"Sending coins to": "Wysyłanie monet do",
"Sending coins": "Sending coins",
"Start Again": "Zacznij jeszcze raz",
"Thank you for using Robosats!": "Dziękujemy za korzystanie z Robosats!",
"Thank you! {{shortAlias}} loves you too": "Dziękujemy! {{shortAlias}} również cię kocha",

View File

@ -56,7 +56,6 @@
"Last order #{{orderID}}": "Última ordem #{{orderID}}",
"Looking for orders!": "Procurando por ordens!",
"No existing orders found": "Nenhuma ordem existente encontrada",
"Recovery": "Recuperação",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Reutilizar a identidade de negociação degrada sua privacidade contra outros usuários, coordenadores e observadores.",
"Robot Garage": "Garagem de Robôs",
"Store your token safely": "Guarde seu token de forma segura",
@ -69,6 +68,7 @@
"Create a new robot and learn to use RoboSats": "Crie um novo robô e aprenda a usar o RoboSats",
"Fast Generate Order": "Gerar Pedido Rápido",
"Recover an existing robot using your token": "Recupere um robô existente usando seu token",
"Recovery": "Recuperação",
"Start": "Iniciar",
"#12": "Phrases in basic/RobotPage/index.tsx",
"Connecting to Tor": "Conectando ao Tor",
@ -444,11 +444,13 @@
"You send approx {{swapSats}} LN Sats (fees might vary)": "Você envia aprox {{swapSats}} LN Sats (as taxas podem variar)",
"#46": "Phrases in components/MakerForm/SelectCoordinator.tsx",
"Disabled": "Desabilitado",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Loading coordinator info...": "Loading coordinator info...",
"Maker": "Criador",
"Onchain payouts enabled": "Pagamentos onchain habilitados",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Taker": "Tomador",
"The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!": "O provedor da infraestrutura de comunicação e lightning. O hospedeiro será responsável por fornecer suporte e resolver disputas. As taxas de negociação são definidas pelo hospedeiro. Certifique-se de selecionar apenas hospedeiros de pedido em quem você confia!",
"This coordinator does not support on-chain swaps.": "Este coordenador não suporta swaps on-chain.",
"#47": "Phrases in components/Notifications/index.tsx",
"Lightning routing failed": "Roteamento Lightning falhou",
"New chat message": "Nova mensagem de chat",
@ -490,16 +492,13 @@
"Accepted payment methods": "Métodos de pagamento aceitos",
"Amount of Satoshis": "Quantidade de Satoshis",
"Deposit": "Depósito",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Expires in": "Expira em",
"F2F location": "Localização presencial",
"Loading cooridnator info...": "Loading cooridnator info...",
"Order Details": "Detalhes da Ordem",
"Order host": "Hospedeiro da Ordem",
"Penalty lifted, good to go!": "Penalidade levantada, pronto para ir!",
"Premium over market price": "Prêmio sobre o preço de mercado",
"Price and Premium": "Preço e Prêmio",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Swap destination": "Destino de Troca",
"The order has expired": "A ordem expirou",
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "A localização fixada é aproximada. A localização exata para o local de encontro deve ser trocada no chat criptografado.",
@ -514,10 +513,10 @@
"Claim": "Reivindicar",
"Enable Telegram Notifications": "Habilitar notificações do Telegram",
"Finished order": "Finished order",
"Generate with Webln": "Gerar com Webln",
"Inactive order": "Ordem inativa",
"Invoice for {{amountSats}} Sats": "Fatura para {{amountSats}} Sats",
"No active orders": "Nenhuma ordem ativa",
"No orders found": "No orders found",
"One active order #{{orderID}}": "Uma ordem ativa #{{orderID}}",
"Submit": "Enviar",
"Telegram enabled": "Telegram ativado",
@ -545,7 +544,6 @@
"{{nickname}} is asking for a collaborative cancel": "{{nickname}} está pedindo um cancelamento colaborativo",
"#54": "Phrases in components/TradeBox/TradeSummary.tsx",
"Buyer": "Comprador",
"Completed in": "Concluído em",
"Contract exchange rate": "Taxa de câmbio do contrato",
"Coordinator trade revenue": "Receita de negociação do coordenador",
"Export trade summary": "Exportar resumo da negociação",
@ -557,7 +555,6 @@
"Seller": "Vendedor",
"Sent": "Enviado",
"Taker bond": "Título do tomador",
"Timestamp": "Carimbo de data/hora",
"Trade Summary": "Resumo da Negociação",
"Unlocked": "Desbloqueado",
"User role": "Função do usuário",
@ -704,7 +701,7 @@
"Rate your trade experience": "Avalie sua experiência de negociação",
"Renew": "Renovar",
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "O RoboSats melhora com mais liquidez e usuários. Conte a um amigo bitcoiner sobre o Robosats!",
"Sending coins to": "Enviando moedas para",
"Sending coins": "Sending coins",
"Start Again": "Comece de novo",
"Thank you for using Robosats!": "Obrigado por usar o Robosats!",
"Thank you! {{shortAlias}} loves you too": "Obrigado! {{shortAlias}} também te ama",

View File

@ -56,7 +56,6 @@
"Last order #{{orderID}}": "Последний ордер #{{orderID}}",
"Looking for orders!": "В поисках ордеров!",
"No existing orders found": "Существующие заказы не найдены",
"Recovery": "Восстановление",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Повторное использование торговых данных ухудшает вашу конфиденциальность по отношению к другим пользователям, координаторам и наблюдателям.",
"Robot Garage": "Гараж роботов",
"Store your token safely": "Храните Ваш токен в безопасности",
@ -69,6 +68,7 @@
"Create a new robot and learn to use RoboSats": "Создайте нового робота и научитесь использовать RoboSats",
"Fast Generate Order": "Быстрое создание ордера",
"Recover an existing robot using your token": "Восстановить существующего робота с помощью вашего токена",
"Recovery": "Восстановление",
"Start": "Старт",
"#12": "Phrases in basic/RobotPage/index.tsx",
"Connecting to Tor": "Подключение к Tor",
@ -444,11 +444,13 @@
"You send approx {{swapSats}} LN Sats (fees might vary)": "Вы отправляете примерно {{swapSats}} LN Сатоши (комиссия может различаться)",
"#46": "Phrases in components/MakerForm/SelectCoordinator.tsx",
"Disabled": "Отключено",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Loading coordinator info...": "Loading coordinator info...",
"Maker": "Мейкер",
"Onchain payouts enabled": "Onchain выплаты включены",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Taker": "Тейкер",
"The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!": "Поставщик инфраструктуры lightning и коммуникации. Хост будет отвечать за предоставление поддержки и решение споров. Торговые комиссии устанавливаются хостом. Убедитесь, что вы выбираете только те хосты, которым доверяете!",
"This coordinator does not support on-chain swaps.": "Этот координатор не поддерживает ончейн свопы.",
"#47": "Phrases in components/Notifications/index.tsx",
"Lightning routing failed": "Маршрутизация Lightning не удалась",
"New chat message": "Новое сообщение в чате",
@ -490,16 +492,13 @@
"Accepted payment methods": "Принимаемые способы оплаты",
"Amount of Satoshis": "Количество Сатоши",
"Deposit": "депозита",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Expires in": "Истекает через",
"F2F location": "Локация F2F",
"Loading cooridnator info...": "Loading cooridnator info...",
"Order Details": "Детали ордера",
"Order host": "Хост ордера",
"Penalty lifted, good to go!": "Пенальти сняты, поехали!",
"Premium over market price": "Наценка над рыночной ценой",
"Price and Premium": "Цена и Наценка",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Swap destination": "Поменять место назначения",
"The order has expired": "Срок действия ордера истёк",
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "Закрепленное местоположение является приблизительным. Точное местоположение места встречи необходимо сообщить в зашифрованном чате.",
@ -514,10 +513,10 @@
"Claim": "Запросить",
"Enable Telegram Notifications": "Включить уведомления Telegram",
"Finished order": "Finished order",
"Generate with Webln": "Генерировать с помощью Webln",
"Inactive order": "Неактивный ордер",
"Invoice for {{amountSats}} Sats": "Инвойс на {{amountSats}} Сатоши",
"No active orders": "Нет активных ордеров",
"No orders found": "No orders found",
"One active order #{{orderID}}": "Один активный ордер #{{orderID}}",
"Submit": "Отправить",
"Telegram enabled": "Telegram включен",
@ -545,7 +544,6 @@
"{{nickname}} is asking for a collaborative cancel": "{{nickname}} запрашивает совместную отмену",
"#54": "Phrases in components/TradeBox/TradeSummary.tsx",
"Buyer": "Покупатель",
"Completed in": "Завершено за",
"Contract exchange rate": "Курс обмена контракта",
"Coordinator trade revenue": "Доход координатора сделки",
"Export trade summary": "Экспортировать сводку торговли",
@ -557,7 +555,6 @@
"Seller": "Продавец",
"Sent": "Отправлено",
"Taker bond": "Залог тейкера",
"Timestamp": "Временная метка",
"Trade Summary": "Сводка сделки",
"Unlocked": "Разблокировано",
"User role": "Роль пользователя",
@ -704,7 +701,7 @@
"Rate your trade experience": "Оцените ваш торговый опыт",
"Renew": "Обновить",
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats становится лучше с большей ликвидностью и пользователями. Расскажите другу-биткойнеру о Robosat!",
"Sending coins to": "Отправка монет на",
"Sending coins": "Sending coins",
"Start Again": "Начать снова",
"Thank you for using Robosats!": "Спасибо за использование Robosats!",
"Thank you! {{shortAlias}} loves you too": "Спасибо! {{shortAlias}} любит вас тоже",

View File

@ -56,7 +56,6 @@
"Last order #{{orderID}}": "Senaste order #{{orderID}}",
"Looking for orders!": "Letar efter ordrar!",
"No existing orders found": "Inga befintliga ordrar hittades",
"Recovery": "Återställning",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Återanvändning av handelsidentitet försämrar din integritet gentemot andra användare, samordnare och observatörer.",
"Robot Garage": "Robotgarage",
"Store your token safely": "Förvara din token säkert",
@ -69,6 +68,7 @@
"Create a new robot and learn to use RoboSats": "Skapa en ny robot och lär dig använda RoboSats",
"Fast Generate Order": "Snabbgenerera order",
"Recover an existing robot using your token": "Återställ en befintlig robot med din token",
"Recovery": "Återställning",
"Start": "Starta",
"#12": "Phrases in basic/RobotPage/index.tsx",
"Connecting to Tor": "Ansluter till Tor",
@ -444,11 +444,13 @@
"You send approx {{swapSats}} LN Sats (fees might vary)": "Du skickar cirka {{swapSats}} LN Sats (avgifter kan variera)",
"#46": "Phrases in components/MakerForm/SelectCoordinator.tsx",
"Disabled": "Inaktiverad",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Loading coordinator info...": "Loading coordinator info...",
"Maker": "Tillverkare",
"Onchain payouts enabled": "Onchain-utbetalningar aktiverade",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Taker": "Tar",
"The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!": "Leverantören av blixt- och kommunikationsinfrastrukturen. Värden kommer att ansvara för att tillhandahålla support och lösa tvister. Handelsavgifterna fastställs av värden. Se till att bara välja ordervärdar som du litar på!",
"This coordinator does not support on-chain swaps.": "Denna koordinator stödjer inte on-chain-byten.",
"#47": "Phrases in components/Notifications/index.tsx",
"Lightning routing failed": "Lightning-routing misslyckades",
"New chat message": "Nytt meddelande i chatten",
@ -490,16 +492,13 @@
"Accepted payment methods": "Accepterade betalningsmetoder",
"Amount of Satoshis": "Belopp av Satoshis",
"Deposit": "Insättningstimer",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Expires in": "Går ut om",
"F2F location": "F2F plats",
"Loading cooridnator info...": "Loading cooridnator info...",
"Order Details": "Orderdetaljer",
"Order host": "Ordervärd",
"Penalty lifted, good to go!": "Straffet undanröjt, bra att gå!",
"Premium over market price": "Premium över marknadspris",
"Price and Premium": "Pris och premium",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Swap destination": "Byt destination",
"The order has expired": "Ordern har gått ut",
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "Den fästa platsen är ungefärlig. Den exakta platsen för mötet måste bytas i den krypterade chatten.",
@ -514,10 +513,10 @@
"Claim": "Anspråk",
"Enable Telegram Notifications": "Aktivera Telegram-aviseringar",
"Finished order": "Finished order",
"Generate with Webln": "Generera med Webln",
"Inactive order": "Inaktiv order",
"Invoice for {{amountSats}} Sats": "Faktura för {{amountSats}} sats",
"No active orders": "Inga aktiva ordrar",
"No orders found": "No orders found",
"One active order #{{orderID}}": "En aktiv order #{{orderID}}",
"Submit": "Skicka",
"Telegram enabled": "Telegram aktiverat",
@ -545,7 +544,6 @@
"{{nickname}} is asking for a collaborative cancel": "{{nickname}} ber om en kollaborativ avbokning",
"#54": "Phrases in components/TradeBox/TradeSummary.tsx",
"Buyer": "Köpare",
"Completed in": "Avslutad i",
"Contract exchange rate": "Kontraktsväxelkurs",
"Coordinator trade revenue": "Koordinators handelsintäkter",
"Export trade summary": "Exportera handelsöversikt",
@ -557,7 +555,6 @@
"Seller": "Säljare",
"Sent": "Skickad",
"Taker bond": "Tagobligation",
"Timestamp": "Tidsstämpel",
"Trade Summary": "Handelsöversikt",
"Unlocked": "Oupplåst",
"User role": "Användarroll",
@ -704,7 +701,7 @@
"Rate your trade experience": "Betygsätt din handelupplevelse",
"Renew": "Förnya",
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats blir bättre med mer likviditet och fler användare. Berätta om RoboSats för en Bitcoiner-vän!",
"Sending coins to": "Skickar mynt till",
"Sending coins": "Sending coins",
"Start Again": "Börja om",
"Thank you for using Robosats!": "Tack för att du använder RoboSats!",
"Thank you! {{shortAlias}} loves you too": "Tack! {{shortAlias}} älskar dig också",

View File

@ -56,7 +56,6 @@
"Last order #{{orderID}}": "Amri ya mwisho #{{orderID}}",
"Looking for orders!": "Inatafuta amri!",
"No existing orders found": "Hakuna amri zilizopo zilizopatikana",
"Recovery": "Kurejesha",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Kutumia tena utambulisho wa biashara hupunguza faragha yako dhidi ya watumiaji wengine, waongozaji na waangalizi.",
"Robot Garage": "Ghala la Roboti",
"Store your token safely": "Hifadhi alama yako kwa usalama",
@ -69,6 +68,7 @@
"Create a new robot and learn to use RoboSats": "Unda roboti mpya na ujifunze kutumia RoboSats",
"Fast Generate Order": "Kuunda Agizo kwa Haraka",
"Recover an existing robot using your token": "Rejesha roboti iliyopo kwa kutumia alama yako",
"Recovery": "Kurejesha",
"Start": "Anza",
"#12": "Phrases in basic/RobotPage/index.tsx",
"Connecting to Tor": "Kuunganisha kwa Tor",
@ -444,11 +444,13 @@
"You send approx {{swapSats}} LN Sats (fees might vary)": "Unatuma takribani {{swapSats}} LN Sats (ada inaweza kutofautiana)",
"#46": "Phrases in components/MakerForm/SelectCoordinator.tsx",
"Disabled": "Imezimwa",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Loading coordinator info...": "Loading coordinator info...",
"Maker": "Muumba",
"Onchain payouts enabled": "Malipo ya mtandaoni yamewezeshwa",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Taker": "Mpokeaji",
"The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!": "Mtoa huduma wa miundombinu ya umeme na mawasiliano. Mwenyeji atakuwa na jukumu la kutoa msaada na kutatua mizozo. Ada za biashara zimewekwa na mwenyeji. Hakikisha unachagua tu wenyeji wa agizo ambao unawaamini!",
"This coordinator does not support on-chain swaps.": "Mratibu huyu hafiati mabadilishano ya mtandaoni.",
"#47": "Phrases in components/Notifications/index.tsx",
"Lightning routing failed": "Njia ya umeme imeshindwa",
"New chat message": "Ujumbe mpya wa gumzo",
@ -490,16 +492,13 @@
"Accepted payment methods": "Njia za malipo zilizokubaliwa",
"Amount of Satoshis": "Kiasi cha Satoshis",
"Deposit": "Amana",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Expires in": "Inamalizika ndani ya",
"F2F location": "Eneo la F2F",
"Loading cooridnator info...": "Loading cooridnator info...",
"Order Details": "Maelezo ya Agizo",
"Order host": "Mwenyeji wa Agizo",
"Penalty lifted, good to go!": "Adhabu imeondolewa, unaweza kuendelea!",
"Premium over market price": "Faida juu ya bei ya soko",
"Price and Premium": "Bei na Faida ya Ziada",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Swap destination": "Marudio ya kubadilisha",
"The order has expired": "Agizo limekwisha muda",
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "Eneo lililowekwa ni takriban. Eneo sahihi la pahala pa mkutano lazima libadilishwe katika gumzo lililofichwa.",
@ -514,10 +513,10 @@
"Claim": "Dai",
"Enable Telegram Notifications": "Washa Arifa za Telegram",
"Finished order": "Finished order",
"Generate with Webln": "Zalisha kwa Webln",
"Inactive order": "Agizo lisilo hai",
"Invoice for {{amountSats}} Sats": "Ankara kwa {{amountSats}} Sats",
"No active orders": "Hakuna maagizo yanayofanya kazi",
"No orders found": "No orders found",
"One active order #{{orderID}}": "Agizo moja la kazi #{{orderID}}",
"Submit": "Wasilisha",
"Telegram enabled": "Telegram imewezeshwa",
@ -545,7 +544,6 @@
"{{nickname}} is asking for a collaborative cancel": "{{nickname}} anaomba kughairi kwa ushirikiano",
"#54": "Phrases in components/TradeBox/TradeSummary.tsx",
"Buyer": "Mnunuzi",
"Completed in": "Imekamilika ndani ya",
"Contract exchange rate": "Kiwango cha kubadilishana kwa mkataba",
"Coordinator trade revenue": "Mapato ya biashara ya mratibu",
"Export trade summary": "Hamisha muhtasari wa biashara",
@ -557,7 +555,6 @@
"Seller": "Muuzaji",
"Sent": "Imetumwa",
"Taker bond": "Dhamana ya Mpokeaji",
"Timestamp": "Wakati wa muhuri",
"Trade Summary": "Muhtasari wa Biashara",
"Unlocked": "Imefunguliwa",
"User role": "Nafasi ya mtumiaji",
@ -704,7 +701,7 @@
"Rate your trade experience": "Panga uzoefu wako wa biashara",
"Renew": "Fanya upya",
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats inaboreshwa kwa utoshelevu zaidi na watumiaji. Mwambie rafiki yako wa Bitcoin kuhusu RoboSats!",
"Sending coins to": "Inatuma sarafu kwa",
"Sending coins": "Sending coins",
"Start Again": "Anza Tena",
"Thank you for using Robosats!": "Asante kwa kutumia Robosats!",
"Thank you! {{shortAlias}} loves you too": "Asante! {{shortAlias}} anakupenda pia",

View File

@ -56,7 +56,6 @@
"Last order #{{orderID}}": "คำสั่งล่าสุด #{{orderID}}",
"Looking for orders!": "กำลังมองหาคำสั่งซื้อ!",
"No existing orders found": "ไม่พบคำสั่งซื้อที่มีอยู่",
"Recovery": "การกู้คืน",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "การใช้ตัวตนการซื้อขายซ้ำจะลดความเป็นส่วนตัวของคุณต่อผู้ใช้คนอื่น ผู้ประสานงานและผู้สังเกตการณ์",
"Robot Garage": "โรงรถหุ่นยนต์",
"Store your token safely": "รักษาโทเค็นของคุณให้ปลอดภัย",
@ -69,6 +68,7 @@
"Create a new robot and learn to use RoboSats": "สร้างหุ่นยนต์ใหม่และเรียนรู้การใช้ RoboSats",
"Fast Generate Order": "สร้างคำสั่งซื้ออย่างรวดเร็ว",
"Recover an existing robot using your token": "กู้คืนหุ่นยนต์ที่มีอยู่โดยใช้โทเค็นของคุณ",
"Recovery": "การกู้คืน",
"Start": "เริ่ม",
"#12": "Phrases in basic/RobotPage/index.tsx",
"Connecting to Tor": "กำลังเชื่อมต่อกับ Tor",
@ -444,11 +444,13 @@
"You send approx {{swapSats}} LN Sats (fees might vary)": "คุณส่งประมาณ {{swapSats}} LN Sats (ค่าธรรมเนียมอาจแตกต่างกัน)",
"#46": "Phrases in components/MakerForm/SelectCoordinator.tsx",
"Disabled": "ปิดการใช้งาน",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Loading coordinator info...": "Loading coordinator info...",
"Maker": "ผู้สร้าง",
"Onchain payouts enabled": "เปิดใช้งานการจ่าย onchain",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Taker": "ผู้รับ",
"The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!": "The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!",
"This coordinator does not support on-chain swaps.": "ผู้ประสานงานนี้ไม่รองรับการสลับ on-chain",
"#47": "Phrases in components/Notifications/index.tsx",
"Lightning routing failed": "การกำหนดเส้นทางสายฟ้าล้มเหลว",
"New chat message": "ข้อความแชทใหม่",
@ -490,16 +492,13 @@
"Accepted payment methods": "วิธีชำระเงินที่ยอมรับได้",
"Amount of Satoshis": "จำนวน Satoshis",
"Deposit": "ผู้ขายต้องวางเหรียญที่จะขายภายใน",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Expires in": "หมดอายุใน",
"F2F location": "ตำแหน่งที่ตั้ง F2F",
"Loading cooridnator info...": "Loading cooridnator info...",
"Order Details": "รายละเอียดคำสั่งซื้อ",
"Order host": "โฮสต์คำสั่งซื้อ",
"Penalty lifted, good to go!": "การลงโทษถูกยกเลิกแล้ว สามารถดำเนินการต่อได้!",
"Premium over market price": "ค่าพรีเมียมจากราคาตลาด",
"Price and Premium": "ราคาและค่าพรีเมียม",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Swap destination": "ปลายทางการแลกเปลี่ยน",
"The order has expired": "คำสั่งซื้อนี้หมดอายุ",
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "ตำแหน่งที่ปักหมุดเป็นค่าประมาณ สถานที่นัดพบที่แน่นอนต้องถูกแลกเปลี่ยนในแชทที่เข้ารหัส",
@ -514,10 +513,10 @@
"Claim": "รับรางวัล",
"Enable Telegram Notifications": "เปิดใช้การแจ้งเตือน Telegram",
"Finished order": "Finished order",
"Generate with Webln": "สร้างด้วย Webln",
"Inactive order": "คำสั่งซื้อที่ไม่ใช้งาน",
"Invoice for {{amountSats}} Sats": "ใบแจ้งหนี้สำหรับ {{amountSats}} Sats",
"No active orders": "ไม่มีคำสั่งซื้อที่ใช้งานอยู่",
"No orders found": "No orders found",
"One active order #{{orderID}}": "มี 1 คำสั่งซื้อที่กำลังดำเนินการอยู่ #{{orderID}}",
"Submit": "ส่งข้อมูล",
"Telegram enabled": "เปิดใช้ Telegram แล้ว",
@ -545,7 +544,6 @@
"{{nickname}} is asking for a collaborative cancel": "{{nickname}} ขอให้คุณยกเลิกการซื้อขายร่วมกัน",
"#54": "Phrases in components/TradeBox/TradeSummary.tsx",
"Buyer": "ผู้ซื้อ",
"Completed in": "เสร็จสิ้นใน",
"Contract exchange rate": "อัตราแลกเปลี่ยนสัญญา",
"Coordinator trade revenue": "รายได้จากการค้าประสานงาน",
"Export trade summary": "ส่งออกสรุปการซื้อขาย",
@ -557,7 +555,6 @@
"Seller": "ผู้ขาย",
"Sent": "ส่งแล้ว",
"Taker bond": "พันธบัตรผู้รับ",
"Timestamp": "ประทับเวลา",
"Trade Summary": "สรุปการซื้อขาย",
"Unlocked": "ปลดล็อกแล้ว",
"User role": "บทบาทผู้ใช้",
@ -704,7 +701,7 @@
"Rate your trade experience": "ให้คะแนนประสบการณ์การค้าของคุณ",
"Renew": "เริ่มใหม่",
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats จะดียิ่งขึ้นเมื่อมีสภาพคล่องและผู้ใช้งานเพิ่มขึ้น บอกเพื่อนบิตคอยเนอร์ของคุณเกี่ยวกับ Robosats!",
"Sending coins to": "กำลังส่งเหรียญไปยัง",
"Sending coins": "Sending coins",
"Start Again": "เริ่มใหม่อีกครั้ง",
"Thank you for using Robosats!": "ขอบคุณที่ใช้ Robosats!",
"Thank you! {{shortAlias}} loves you too": "ขอบคุณ! {{shortAlias}} รักคุณเช่นกัน",

View File

@ -56,7 +56,6 @@
"Last order #{{orderID}}": "上一张订单 #{{orderID}}",
"Looking for orders!": "查找订单!",
"No existing orders found": "未发现现有订单",
"Recovery": "恢复",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "重复使用交易身份会降低您相对其他用户、协调员和观察者的隐私。",
"Robot Garage": "机器人车库",
"Store your token safely": "安全地存储您的令牌",
@ -69,6 +68,7 @@
"Create a new robot and learn to use RoboSats": "创建一个新的机器人并学习如何使用 RoboSats",
"Fast Generate Order": "快速生成订单",
"Recover an existing robot using your token": "用您的令牌恢复现有的机器人",
"Recovery": "恢复",
"Start": "开始",
"#12": "Phrases in basic/RobotPage/index.tsx",
"Connecting to Tor": "正在连接 Tor",
@ -444,11 +444,13 @@
"You send approx {{swapSats}} LN Sats (fees might vary)": "你将发送大约{{swapSats}}闪电聪(费用会造成差异)",
"#46": "Phrases in components/MakerForm/SelectCoordinator.tsx",
"Disabled": "已禁用",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Loading coordinator info...": "Loading coordinator info...",
"Maker": "挂单方",
"Onchain payouts enabled": "链上支付已启用",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Taker": "吃单方",
"The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!": "提供闪电和通信基础设施。主机会负责提供支持和解决争议。交易费用由主机设置。请确保仅选择您信任的订单主机!",
"This coordinator does not support on-chain swaps.": "此协调员不支持链上交换。",
"#47": "Phrases in components/Notifications/index.tsx",
"Lightning routing failed": "闪电路由失败",
"New chat message": "新消息",
@ -490,16 +492,13 @@
"Accepted payment methods": "接受的支付方式",
"Amount of Satoshis": "聪金额",
"Deposit": "定金截止时间",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Expires in": "内到期",
"F2F location": "面对面位置",
"Loading cooridnator info...": "Loading cooridnator info...",
"Order Details": "订单详情",
"Order host": "订单主机",
"Penalty lifted, good to go!": "罚款已解除,可以继续!",
"Premium over market price": "高于市价的溢价",
"Price and Premium": "价格和溢价",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Swap destination": "交换目的地",
"The order has expired": "订单已到期",
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "固定位置是近似的。会面地点的确切位置必须在加密聊天中交换。",
@ -514,10 +513,10 @@
"Claim": "领取",
"Enable Telegram Notifications": "开启电报通知",
"Finished order": "Finished order",
"Generate with Webln": "使用 Webln 生成",
"Inactive order": "不活跃订单",
"Invoice for {{amountSats}} Sats": "{{amountSats}}聪的发票",
"No active orders": "没有活跃的订单",
"No orders found": "No orders found",
"One active order #{{orderID}}": "一个活跃的订单#{{orderID}}",
"Submit": "提交",
"Telegram enabled": "电报已启用",
@ -545,7 +544,6 @@
"{{nickname}} is asking for a collaborative cancel": "{{nickname}} 要求合作取消",
"#54": "Phrases in components/TradeBox/TradeSummary.tsx",
"Buyer": "买方",
"Completed in": "完成于",
"Contract exchange rate": "合约交易率",
"Coordinator trade revenue": "协调员交易收入",
"Export trade summary": "导出交易总结",
@ -557,7 +555,6 @@
"Seller": "卖方",
"Sent": "已发送",
"Taker bond": "吃单方保证金",
"Timestamp": "时间戳",
"Trade Summary": "交易总结",
"Unlocked": "已解锁",
"User role": "用户角色",
@ -704,7 +701,7 @@
"Rate your trade experience": "评价您的交易体验",
"Renew": "延续",
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats会随着更多的流动性和更高的用户数量而变得更好。把RoboSats推荐给您的比特币朋友吧",
"Sending coins to": "将比特币发送到",
"Sending coins": "Sending coins",
"Start Again": "重新开始",
"Thank you for using Robosats!": "感谢您使用Robosats",
"Thank you! {{shortAlias}} loves you too": "谢谢您!{{shortAlias}}也爱您",

View File

@ -56,7 +56,6 @@
"Last order #{{orderID}}": "上一張訂單 #{{orderID}}",
"Looking for orders!": "找尋訂單!",
"No existing orders found": "未找到現有訂單",
"Recovery": "恢復",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "重複使用交易身份會降低你的對其他用戶、協調器和觀察者的隱私。",
"Robot Garage": "機器人倉庫",
"Store your token safely": "請安全地存儲你的領牌",
@ -69,6 +68,7 @@
"Create a new robot and learn to use RoboSats": "創建一個新的機器人並學習如何使用 RoboSats",
"Fast Generate Order": "快速生成訂單",
"Recover an existing robot using your token": "用你的領牌恢復現有的機器人",
"Recovery": "恢復",
"Start": "開始",
"#12": "Phrases in basic/RobotPage/index.tsx",
"Connecting to Tor": "正在連線 Tor",
@ -444,11 +444,13 @@
"You send approx {{swapSats}} LN Sats (fees might vary)": "您將發送大約{{swapSats}} 閃電聰(費用可能有所不同)",
"#46": "Phrases in components/MakerForm/SelectCoordinator.tsx",
"Disabled": "禁用的",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Loading coordinator info...": "Loading coordinator info...",
"Maker": "掛單方",
"Onchain payouts enabled": "Onchain payouts enabled",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Taker": "吃單方",
"The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!": "提供者提供閃電與通信架構。 主機將負責提供支持和解決爭議。 交易費用由主機設定。 务必遵循信任主機的原則选择订单主机!",
"This coordinator does not support on-chain swaps.": "此協調者不支持鏈上交換。",
"#47": "Phrases in components/Notifications/index.tsx",
"Lightning routing failed": "閃電路由失敗",
"New chat message": "新聊天消息",
@ -490,16 +492,13 @@
"Accepted payment methods": "接受的付款方法",
"Amount of Satoshis": "聰的金額",
"Deposit": "存款",
"Does not support on-chain swaps.": "Does not support on-chain swaps.",
"Expires in": "到期於",
"F2F location": "見面地點",
"Loading cooridnator info...": "Loading cooridnator info...",
"Order Details": "訂單詳情",
"Order host": "訂單主機",
"Penalty lifted, good to go!": "罰單已解除,可以開始!",
"Premium over market price": "高於市價的溢價",
"Price and Premium": "價格和溢價",
"Supports on-chain swaps.": "Supports on-chain swaps.",
"Swap destination": "交換目的地",
"The order has expired": "訂單已到期",
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "釘定的位置是近似的。 聚會地點的確切位置必須在加密聊天中交換。",
@ -514,10 +513,10 @@
"Claim": "索取",
"Enable Telegram Notifications": "啟用 Telegram 通知",
"Finished order": "Finished order",
"Generate with Webln": "使用 Webln 生成",
"Inactive order": "不活躍的訂單",
"Invoice for {{amountSats}} Sats": "{{amountSats}} 聰的發票",
"No active orders": "沒有活躍的訂單",
"No orders found": "No orders found",
"One active order #{{orderID}}": "一個活躍的訂單 #{{orderID}}",
"Submit": "提交",
"Telegram enabled": "Telegram 已啟用",
@ -545,7 +544,6 @@
"{{nickname}} is asking for a collaborative cancel": "{{nickname}} 要求合作取消",
"#54": "Phrases in components/TradeBox/TradeSummary.tsx",
"Buyer": "買方",
"Completed in": "完成內",
"Contract exchange rate": "合約交易率",
"Coordinator trade revenue": "協調器交易收入",
"Export trade summary": "導出交易概要",
@ -557,7 +555,6 @@
"Seller": "賣方",
"Sent": "已發送",
"Taker bond": "吃單方保證金",
"Timestamp": "時間戳",
"Trade Summary": "交易概述",
"Unlocked": "已解鎖",
"User role": "用戶角色",
@ -704,7 +701,7 @@
"Rate your trade experience": "評價您的交易體驗",
"Renew": "延續",
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats 會隨著更多的流動性和更高的用戶数量而變得更好。把 RoboSats 推薦給您的比特幣朋友吧!",
"Sending coins to": "正在將比特幣發送到",
"Sending coins": "Sending coins",
"Start Again": "重新開始",
"Thank you for using Robosats!": "感謝您使用 Robosats!",
"Thank you! {{shortAlias}} loves you too": "謝謝你!{{shortAlias}} 也愛你",

View File

@ -2,15 +2,16 @@
<html>
<head>
<meta http-equiv="onion-location" content="{{ ONION_LOCATION }}" />
<link rel="shortcut icon" href="<%= htmlWebpackPlugin.options.basePath %>static/assets/images/favicon-96x96.png" />
<link rel="icon" type="image/png" href="<%= htmlWebpackPlugin.options.basePath %>static/assets/images/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="<%= htmlWebpackPlugin.options.basePath %>static/assets/images/favicon-96x96.png" sizes="96x96">
<link rel="icon" type="image/png" href="<%= htmlWebpackPlugin.options.basePath %>static/assets/images/favicon-192x192.png" sizes="192x192">
<% if (!mobile) { %>
<link rel="shortcut icon" href="<%= htmlWebpackPlugin.options.basePath %>static/assets/images/favicon-96x96.png" />
<link rel="icon" type="image/png" href="<%= htmlWebpackPlugin.options.basePath %>static/assets/images/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="<%= htmlWebpackPlugin.options.basePath %>static/assets/images/favicon-96x96.png" sizes="96x96">
<link rel="icon" type="image/png" href="<%= htmlWebpackPlugin.options.basePath %>static/assets/images/favicon-192x192.png" sizes="192x192">
<% } %>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="A simple and private way to exchange bitcoin for national currencies. Robosats simplifies the peer-to-peer user experience and uses lightning hold invoices to minimize custody and trust requirements. No user registration required.">
<% if (pro) { %>
<title>RoboSats PRO - Simple and Private Bitcoin Exchange</title>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.basePath %>static/css_pro/fonts.css"/>
@ -24,7 +25,7 @@
<link rel="stylesheet" type="text/css" href="<%= htmlWebpackPlugin.options.basePath %>static/css/loader.css"/>
<link rel="stylesheet" type="text/css" href="<%= htmlWebpackPlugin.options.basePath %>static/css/index.css"/>
<link rel="stylesheet" type="text/css" href="<%= htmlWebpackPlugin.options.basePath %>static/css/leaflet.css"/>
<style>
/* Styles for the JavaScript-disabled disclaimer */
.noscript-container {
@ -61,38 +62,40 @@
<p>Need more help? Visit our support channel on Telegram: <a href="https://t.me/robosats" target="_blank" rel="noopener">t.me/robosats</a>.</p>
</div>
</noscript>
<div id="main">
<div id="app">
<div class="loaderCenter">
<div class="loaderSpinner"></div>
<div class="content-slider">
<div class="slider">
<div class="mask">
<ul>
<li class="anim1">
<div class="quote">Looking for robot parts ...</div>
</li>
<li class="anim2">
<div class="quote">Adding layers to the onion ...</div>
</li>
<li class="anim3">
<div class="quote">Winning at game theory ...</div>
</li>
<li class="anim4">
<div class="quote">Moving Sats at light speed ...</div>
</li>
<li class="anim5">
<div class="quote">Hiding in 2^256 bits of entropy...</div>
</li>
</ul>
<% if (!mobile) { %>
<div class="loaderSpinner"></div>
<div class="content-slider">
<div class="slider">
<div class="mask">
<ul>
<li class="anim1">
<div class="quote">Looking for robot parts ...</div>
</li>
<li class="anim2">
<div class="quote">Adding layers to the onion ...</div>
</li>
<li class="anim3">
<div class="quote">Winning at game theory ...</div>
</li>
<li class="anim4">
<div class="quote">Moving Sats at light speed ...</div>
</li>
<li class="anim5">
<div class="quote">Hiding in 2^256 bits of entropy...</div>
</li>
</ul>
</div>
</div>
</div>
<% } %>
</div>
</div>
</div>
</div>
<script>
window.RobosatsSettings = '<%= htmlWebpackPlugin.options.robosatsSettings %>';
</script>

View File

@ -34,212 +34,225 @@ const config: Configuration = {
},
};
const configNode: Configuration = {
...config,
output: {
path: path.resolve(__dirname, 'static/frontend'),
filename: `main.v${version}.[contenthash].js`,
clean: true,
publicPath: '/static/frontend/',
},
plugins: [
// Django
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'templates/frontend/index.ejs'),
templateParameters: {
pro: false,
},
filename: path.resolve(__dirname, 'templates/frontend/basic.html'),
inject: 'body',
robosatsSettings: 'web-basic',
basePath: '/',
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'templates/frontend/index.ejs'),
templateParameters: {
pro: true,
},
filename: path.resolve(__dirname, 'templates/frontend/pro.html'),
inject: 'body',
robosatsSettings: 'web-pro',
basePath: '/',
}),
// Node App
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'templates/frontend/index.ejs'),
templateParameters: {
pro: false,
},
filename: path.resolve(__dirname, '../nodeapp/basic.html'),
inject: 'body',
robosatsSettings: 'selfhosted-basic',
basePath: '/',
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'templates/frontend/index.ejs'),
templateParameters: {
pro: true,
},
filename: path.resolve(__dirname, '../nodeapp/pro.html'),
inject: 'body',
robosatsSettings: 'selfhosted-pro',
basePath: '/',
}),
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, 'static'),
to: path.resolve(__dirname, '../nodeapp/static'),
const configNode = (env: any, argv: { mode: string }): Configuration => {
return {
...config,
output: {
path: path.resolve(__dirname, 'static/frontend'),
filename:
argv.mode === 'production' ? `main.v${version}.[contenthash].js` : `main.v${version}.js`,
clean: true,
publicPath: '/static/frontend/',
},
plugins: [
// Django
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'templates/frontend/index.ejs'),
templateParameters: {
pro: false,
mobile: false,
},
],
}),
// Desktop App
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'templates/frontend/index.ejs'),
templateParameters: {
pro: false,
},
filename: path.resolve(__dirname, '../desktopApp/index.html'),
inject: 'body',
robosatsSettings: 'desktop-basic',
basePath: '/',
}),
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, 'static'),
to: path.resolve(__dirname, '../desktopApp/static'),
filename: path.resolve(__dirname, 'templates/frontend/basic.html'),
inject: 'body',
robosatsSettings: 'web-basic',
basePath: '/',
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'templates/frontend/index.ejs'),
templateParameters: {
pro: true,
mobile: false,
},
],
}),
// Web App
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'templates/frontend/index.ejs'),
templateParameters: {
pro: false,
},
filename: path.resolve(__dirname, '../web/basic.html'),
inject: 'body',
robosatsSettings: 'web-basic',
basePath: '/',
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'templates/frontend/index.ejs'),
templateParameters: {
pro: true,
},
filename: path.resolve(__dirname, '../web/pro.html'),
inject: 'body',
robosatsSettings: 'web-pro',
basePath: '/',
}),
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, 'static'),
to: path.resolve(__dirname, '../web/static'),
filename: path.resolve(__dirname, 'templates/frontend/pro.html'),
inject: 'body',
robosatsSettings: 'web-pro',
basePath: '/',
}),
// Node App
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'templates/frontend/index.ejs'),
templateParameters: {
pro: false,
mobile: false,
},
],
}),
],
filename: path.resolve(__dirname, '../nodeapp/basic.html'),
inject: 'body',
robosatsSettings: 'selfhosted-basic',
basePath: '/',
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'templates/frontend/index.ejs'),
templateParameters: {
pro: true,
mobile: false,
},
filename: path.resolve(__dirname, '../nodeapp/pro.html'),
inject: 'body',
robosatsSettings: 'selfhosted-pro',
basePath: '/',
}),
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, 'static'),
to: path.resolve(__dirname, '../nodeapp/static'),
},
],
}),
// Desktop App
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'templates/frontend/index.ejs'),
templateParameters: {
pro: false,
mobile: false,
},
filename: path.resolve(__dirname, '../desktopApp/index.html'),
inject: 'body',
robosatsSettings: 'desktop-basic',
basePath: '/',
}),
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, 'static'),
to: path.resolve(__dirname, '../desktopApp/static'),
},
],
}),
// Web App
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'templates/frontend/index.ejs'),
templateParameters: {
pro: false,
mobile: false,
},
filename: path.resolve(__dirname, '../web/basic.html'),
inject: 'body',
robosatsSettings: 'web-basic',
basePath: '/',
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'templates/frontend/index.ejs'),
templateParameters: {
pro: true,
mobile: false,
},
filename: path.resolve(__dirname, '../web/pro.html'),
inject: 'body',
robosatsSettings: 'web-pro',
basePath: '/',
}),
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, 'static'),
to: path.resolve(__dirname, '../web/static'),
},
],
}),
],
};
};
const configAndroid: Configuration = {
...config,
module: {
...config.module,
rules: [
...(config?.module?.rules || []),
{
test: path.resolve(__dirname, 'src/i18n/Web.js'),
loader: 'file-replace-loader',
options: {
condition: 'if-replacement-exists',
replacement: path.resolve(__dirname, 'src/i18n/Mobile.js'),
async: true,
},
},
{
test: path.resolve(__dirname, 'src/geo/Web.js'),
loader: 'file-replace-loader',
options: {
condition: 'if-replacement-exists',
replacement: path.resolve(__dirname, 'src/geo/Mobile.js'),
async: true,
},
},
{
test: path.resolve(__dirname, 'src/services/Roboidentities/Web.ts'),
loader: 'file-replace-loader',
options: {
condition: 'if-replacement-exists',
replacement: path.resolve(__dirname, 'src/services/Roboidentities/Android.ts'),
async: true,
},
},
{
test: path.resolve(__dirname, 'src/components/RobotAvatar/placeholder.json'),
loader: 'file-replace-loader',
options: {
condition: 'if-replacement-exists',
replacement: path.resolve(
__dirname,
'src/components/RobotAvatar/placeholder_highres.json',
),
async: true,
},
},
],
},
output: {
path: path.resolve(__dirname, '../android/app/src/main/assets/static/frontend'),
filename: `main.v${version}.[contenthash].js`,
clean: true,
publicPath: './static/frontend/',
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'templates/frontend/index.ejs'),
templateParameters: {
pro: false,
},
filename: path.resolve(__dirname, '../android/app/src/main/assets/index.html'),
inject: 'body',
robosatsSettings: 'mobile-basic',
basePath: 'file:///android_asset/',
}),
new CopyWebpackPlugin({
patterns: [
const configAndroid = (env: any, argv: { mode: string }): Configuration => {
return {
...config,
module: {
...config.module,
rules: [
...(config?.module?.rules || []),
{
from: path.resolve(__dirname, 'static/css'),
to: path.resolve(__dirname, '../android/app/src/main/assets/static/css'),
transform(content, path) {
if (path.endsWith('.css')) {
let cssContent = content.toString();
cssContent = cssContent.replace(
/url\(\/static\/css\/fonts\/roboto/g,
'url(file:///android_asset/static/css/fonts/roboto',
);
return Buffer.from(cssContent);
}
return content;
test: path.resolve(__dirname, 'src/i18n/Web.js'),
loader: 'file-replace-loader',
options: {
condition: 'if-replacement-exists',
replacement: path.resolve(__dirname, 'src/i18n/Mobile.js'),
async: true,
},
},
{
from: path.resolve(__dirname, 'static/assets/sounds'),
to: path.resolve(__dirname, '../android/app/src/main/assets/static/assets/sounds'),
test: path.resolve(__dirname, 'src/geo/Web.js'),
loader: 'file-replace-loader',
options: {
condition: 'if-replacement-exists',
replacement: path.resolve(__dirname, 'src/geo/Mobile.js'),
async: true,
},
},
{
from: path.resolve(__dirname, 'static/assets/images/favicon-*'),
to: path.resolve(__dirname, '../android/app/src/main/assets'),
test: path.resolve(__dirname, 'src/services/Roboidentities/Web.ts'),
loader: 'file-replace-loader',
options: {
condition: 'if-replacement-exists',
replacement: path.resolve(__dirname, 'src/services/Roboidentities/Android.ts'),
async: true,
},
},
{
from: path.resolve(__dirname, 'static/federation'),
to: path.resolve(__dirname, '../android/app/src/main/assets/static/assets/federation'),
test: path.resolve(__dirname, 'src/components/RobotAvatar/placeholder.json'),
loader: 'file-replace-loader',
options: {
condition: 'if-replacement-exists',
replacement: path.resolve(
__dirname,
'src/components/RobotAvatar/placeholder_highres.json',
),
async: true,
},
},
],
}),
],
},
output: {
path: path.resolve(__dirname, '../android/app/src/main/assets/static/frontend'),
filename:
argv.mode === 'production' ? `main.v${version}.[contenthash].js` : `main.v${version}.js`,
clean: true,
publicPath: './static/frontend/',
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'templates/frontend/index.ejs'),
templateParameters: {
pro: false,
mobile: true,
},
filename: path.resolve(__dirname, '../android/app/src/main/assets/index.html'),
inject: 'body',
robosatsSettings: 'mobile-basic',
basePath: 'file:///android_asset/',
}),
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, 'static/css'),
to: path.resolve(__dirname, '../android/app/src/main/assets/static/css'),
transform(content, path) {
if (path.endsWith('.css')) {
let cssContent = content.toString();
cssContent = cssContent.replace(
/url\(\/static\/css\/fonts\/roboto/g,
'url(file:///android_asset/static/css/fonts/roboto',
);
return Buffer.from(cssContent);
}
return content;
},
},
{
from: path.resolve(__dirname, 'static/assets/sounds'),
to: path.resolve(__dirname, '../android/app/src/main/assets/static/assets/sounds'),
},
{
from: path.resolve(__dirname, 'static/federation'),
to: path.resolve(__dirname, '../android/app/src/main/assets/static/assets/federation'),
},
],
}),
],
};
};
export default [configNode, configAndroid];
export default (env: any, argv: { mode: string }) => [
configNode(env, argv),
configAndroid(env, argv),
];