mirror of
https://github.com/RoboSats/robosats.git
synced 2025-09-13 00:56:22 +00:00
Encrypted Storage
This commit is contained in:
@ -66,7 +66,6 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
@ -74,6 +73,7 @@ dependencies {
|
||||
implementation(libs.kmp.tor)
|
||||
implementation(libs.quartz)
|
||||
implementation(libs.ammolite)
|
||||
implementation(libs.security.crypto.ktx)
|
||||
// Add the KMP Tor binary dependency (contains the native .so files)
|
||||
implementation(libs.kmp.tor.binary)
|
||||
implementation(libs.androidx.activity)
|
||||
|
||||
@ -28,6 +28,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.robosats.models.EncryptedStorage
|
||||
import com.robosats.services.NotificationsService
|
||||
import com.robosats.tor.TorKmp
|
||||
import com.robosats.tor.TorKmpManager
|
||||
@ -42,6 +43,8 @@ class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
EncryptedStorage.init(this)
|
||||
|
||||
// Lock the screen orientation to portrait mode
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
|
||||
@ -73,6 +76,9 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Notifications service
|
||||
*/
|
||||
private fun initializeNotifications() {
|
||||
startForegroundService(
|
||||
Intent(
|
||||
@ -82,8 +88,10 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize TorKmp if it's not already initialized
|
||||
*/
|
||||
private fun initializeTor() {
|
||||
// Initialize TorKmp if it's not already initialized
|
||||
try {
|
||||
try {
|
||||
torKmp = TorKmpManager.getTorKmpObject()
|
||||
@ -170,6 +178,9 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures initial WebView settings with external blocked
|
||||
*/
|
||||
private fun setupWebView() {
|
||||
// Double-check Tor is connected before proceeding
|
||||
if (!torKmp.isConnected()) {
|
||||
@ -367,8 +378,6 @@ class MainActivity : AppCompatActivity() {
|
||||
webSettings.textZoom = 100
|
||||
}
|
||||
|
||||
// SSL error description method removed as we're not using SSL
|
||||
|
||||
/**
|
||||
* Clear all WebView data when activity is destroyed
|
||||
*/
|
||||
|
||||
@ -6,6 +6,7 @@ import android.util.Log
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import com.robosats.models.EncryptedStorage
|
||||
import com.robosats.tor.TorKmpManager.getTorKmpObject
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
@ -318,6 +319,64 @@ class WebAppInterface(private val context: Context, private val webView: WebView
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun getEncryptedStorage(uuid: String, key: String) {
|
||||
// Validate inputs before processing
|
||||
if (!isValidUuid(uuid) || !isValidInput(key)) {
|
||||
Log.e(TAG, "Invalid input for getEncryptedStorage: uuid=$uuid, key=$key")
|
||||
rejectPromise(uuid, "Invalid input parameters")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Sanitize the input before passing to native code
|
||||
val sanitizedKey = key.trim()
|
||||
|
||||
val value = EncryptedStorage.getEncryptedStorage(sanitizedKey)
|
||||
|
||||
// Safely encode and return the result
|
||||
resolvePromise(uuid, value)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in getEncryptedStorage", e)
|
||||
rejectPromise(uuid, "Error obtaining encrypted storage: $key")
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun setEncryptedStorage(uuid: String, key: String, value: String) {
|
||||
// Validate inputs before processing
|
||||
if (!isValidUuid(uuid) || !isValidInput(key)) {
|
||||
Log.e(TAG, "Invalid input for setEncryptedStorage: uuid=$uuid, key=$key")
|
||||
rejectPromise(uuid, "Invalid input parameters")
|
||||
return
|
||||
}
|
||||
// Sanitize the input before passing to native code
|
||||
val sanitizedKey = key.trim()
|
||||
val sanitizedValue = value.trim()
|
||||
|
||||
EncryptedStorage.setEncryptedStorage(sanitizedKey, sanitizedValue)
|
||||
|
||||
// Safely encode and return the result
|
||||
resolvePromise(uuid, key)
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun deleteEncryptedStorage(uuid: String, key: String) {
|
||||
// Validate inputs before processing
|
||||
if (!isValidUuid(uuid) || !isValidInput(key)) {
|
||||
Log.e(TAG, "Invalid input for deleteEncryptedStorage: uuid=$uuid, key=$key")
|
||||
rejectPromise(uuid, "Invalid input parameters")
|
||||
return
|
||||
}
|
||||
// Sanitize the input before passing to native code
|
||||
val sanitizedKey = key.trim()
|
||||
|
||||
EncryptedStorage.deleteEncryptedStorage(sanitizedKey)
|
||||
|
||||
// Safely encode and return the result
|
||||
resolvePromise(uuid, key)
|
||||
}
|
||||
|
||||
private fun onWsMessage(path: String?, message: String?) {
|
||||
val encodedMessage = encodeForJavaScript(message)
|
||||
safeEvaluateJavascript("javascript:window.AndroidRobosats.onWSMessage('$path', '$encodedMessage')")
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
package com.robosats.models
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
|
||||
|
||||
object EncryptedStorage {
|
||||
private const val PREFERENCES_NAME = "secret_keeper"
|
||||
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
|
||||
|
||||
fun init(context: Context) {
|
||||
val masterKey: MasterKey =
|
||||
MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build()
|
||||
|
||||
sharedPreferences = EncryptedSharedPreferences.create(
|
||||
context,
|
||||
PREFERENCES_NAME,
|
||||
masterKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||
) as EncryptedSharedPreferences
|
||||
}
|
||||
|
||||
fun setEncryptedStorage(key: String, value: String) {
|
||||
sharedPreferences.edit { putString(key, value) }
|
||||
}
|
||||
|
||||
fun getEncryptedStorage(key: String): String {
|
||||
return sharedPreferences.getString(key, "") ?: ""
|
||||
}
|
||||
|
||||
fun deleteEncryptedStorage(key: String) {
|
||||
sharedPreferences.edit { remove(key) }
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,7 @@ import com.vitorpamplona.ammolite.relays.Relay
|
||||
import com.vitorpamplona.ammolite.relays.RelayPool
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import org.json.JSONObject
|
||||
|
||||
object NostrClient {
|
||||
private var subscriptionNotificationId = "robosatsNotificationId"
|
||||
@ -41,6 +42,10 @@ object NostrClient {
|
||||
}
|
||||
|
||||
private fun connectRelays() {
|
||||
val garageString = EncryptedStorage.getEncryptedStorage("garage_slots")
|
||||
|
||||
val garage = JSONObject(garageString)
|
||||
|
||||
val relays = emptyList<String>()
|
||||
|
||||
relays.forEach {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
agp = "8.11.1"
|
||||
kotlin = "2.0.21"
|
||||
coreKtx = "1.16.0"
|
||||
securityCryptoKtx = "1.1.0-beta01"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.2.1"
|
||||
espressoCore = "3.6.1"
|
||||
@ -28,6 +29,7 @@ kmp-tor-binary = { group = "io.matthewnelson.kotlin-components", name = "kmp-tor
|
||||
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||
quartz = { module = "com.github.vitorpamplona.amethyst:quartz", version.ref = "quartz" }
|
||||
ammolite = { module = "com.github.vitorpamplona.amethyst:ammolite", version.ref = "quartz" }
|
||||
security-crypto-ktx = { module = "androidx.security:security-crypto-ktx", version.ref = "securityCryptoKtx" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
@ -59,14 +59,13 @@ const NotificationsDrawer = ({
|
||||
const [openSnak, setOpenSnak] = React.useState<boolean>(false);
|
||||
const [snakEvent, setSnakevent] = React.useState<Event>();
|
||||
const [subscribedTokens, setSubscribedTokens] = React.useState<string[]>([]);
|
||||
const [_, setLastNotification] = React.useState<number>(
|
||||
parseInt(
|
||||
systemClient.getItem('last_notification') === ''
|
||||
? '0'
|
||||
: (systemClient.getItem('last_notification') ?? '0'),
|
||||
10,
|
||||
),
|
||||
);
|
||||
const [_, setLastNotification] = React.useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
systemClient.getItem('last_notification').then((result) => {
|
||||
setLastNotification(!result || result === '' ? 0 : parseInt(result, 10));
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setShow(false);
|
||||
|
||||
@ -8,9 +8,9 @@ const SelfhostedAlert = (): React.JSX.Element => {
|
||||
const [show, setShow] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!systemClient.getItem('selfhosted-alert')) {
|
||||
setShow(true);
|
||||
}
|
||||
systemClient.getItem('selfhosted-alert').then((result) => {
|
||||
if (result) setShow(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// If alert is hidden return null
|
||||
|
||||
@ -41,9 +41,9 @@ const UnsafeAlert = (): React.JSX.Element => {
|
||||
const [unsafeClient, setUnsafeClient] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!systemClient.getItem('unsafe-alert')) {
|
||||
setShow(true);
|
||||
}
|
||||
systemClient.getItem('unsafe-alert').then((result) => {
|
||||
if (result) setShow(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const checkClient = (): void => {
|
||||
|
||||
@ -8,7 +8,6 @@ import {
|
||||
defaultExchange,
|
||||
} from '.';
|
||||
import defaultFederation from '../../static/federation.json';
|
||||
import { systemClient } from '../services/System';
|
||||
import { federationLottery, getHost } from '../utils';
|
||||
import { coordinatorDefaultValues } from './Coordinator.model';
|
||||
import { updateExchangeInfo } from './Exchange.model';
|
||||
@ -98,9 +97,6 @@ export class Federation {
|
||||
} else {
|
||||
void this.loadBook();
|
||||
}
|
||||
|
||||
const federationUrls = Object.values(this.coordinators).map((c) => c.url);
|
||||
systemClient.setCookie('federation', JSON.stringify(federationUrls));
|
||||
};
|
||||
|
||||
refreshBookHosts: (robosatsOnly: boolean) => void = (robosatsOnly) => {
|
||||
|
||||
@ -51,9 +51,9 @@ class Garage {
|
||||
this.triggerHook('onSlotUpdate');
|
||||
};
|
||||
|
||||
loadSlots = (): void => {
|
||||
loadSlots = async (): Promise<void> => {
|
||||
this.slots = {};
|
||||
const slotsDump: string = systemClient.getItem('garage_slots') ?? '';
|
||||
const slotsDump: string = (await systemClient.getItem('garage_slots')) ?? '';
|
||||
|
||||
if (slotsDump !== '') {
|
||||
const rawSlots: Record<string, object> = JSON.parse(slotsDump);
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import { systemClient } from '../services/System';
|
||||
import BaseSettings from './Settings.model';
|
||||
|
||||
class SettingsSelfhosted extends BaseSettings {
|
||||
constructor() {
|
||||
super();
|
||||
const fontSizeCookie = systemClient.getItem('settings_fontsize_basic');
|
||||
this.fontSize = fontSizeCookie !== '' ? Number(fontSizeCookie) : 14;
|
||||
}
|
||||
|
||||
public frontend: 'basic' | 'pro' = 'basic';
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import { systemClient } from '../services/System';
|
||||
import BaseSettings from './Settings.model';
|
||||
|
||||
class Settings extends BaseSettings {
|
||||
constructor() {
|
||||
super();
|
||||
const fontSizeCookie = systemClient.getItem('settings_fontsize_basic');
|
||||
this.fontSize = fontSizeCookie !== '' ? Number(fontSizeCookie) : 14;
|
||||
}
|
||||
|
||||
public frontend: 'basic' | 'pro' = 'basic';
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import { systemClient } from '../services/System';
|
||||
import BaseSettings from './Settings.model';
|
||||
|
||||
class SettingsSelfhostedPro extends BaseSettings {
|
||||
constructor() {
|
||||
super();
|
||||
const fontSizeCookie = systemClient.getItem('settings_fontsize_pro');
|
||||
this.fontSize = fontSizeCookie !== '' ? Number(fontSizeCookie) : 12;
|
||||
}
|
||||
|
||||
public frontend: 'basic' | 'pro' = 'pro';
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import { systemClient } from '../services/System';
|
||||
import BaseSettings from './Settings.model';
|
||||
|
||||
class SettingsPro extends BaseSettings {
|
||||
constructor() {
|
||||
super();
|
||||
const fontSizeCookie = systemClient.getItem('settings_fontsize_pro');
|
||||
this.fontSize = fontSizeCookie !== '' ? Number(fontSizeCookie) : 12;
|
||||
}
|
||||
|
||||
public frontend: 'basic' | 'pro' = 'pro';
|
||||
|
||||
@ -25,45 +25,57 @@ export type Language =
|
||||
|
||||
class BaseSettings {
|
||||
constructor() {
|
||||
const modeCookie: 'light' | 'dark' | '' = systemClient.getItem('settings_mode');
|
||||
this.mode =
|
||||
modeCookie !== ''
|
||||
? modeCookie
|
||||
: window?.matchMedia('(prefers-color-scheme: dark)')?.matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
const [client] = window.RobosatsSettings.split('-');
|
||||
this.client = client as 'web' | 'mobile';
|
||||
|
||||
this.lightQRs = systemClient.getItem('settings_light_qr') === 'true';
|
||||
|
||||
const languageCookie = systemClient.getItem('settings_language');
|
||||
this.language =
|
||||
languageCookie !== ''
|
||||
? languageCookie
|
||||
: i18n.resolvedLanguage == null
|
||||
? 'en'
|
||||
: i18n.resolvedLanguage.substring(0, 2);
|
||||
|
||||
const connection = systemClient.getItem('settings_connection');
|
||||
this.connection = connection && connection !== '' ? connection : this.connection;
|
||||
|
||||
const networkCookie = systemClient.getItem('settings_network');
|
||||
this.network = networkCookie && networkCookie !== '' ? networkCookie : this.network;
|
||||
this.host = getHost();
|
||||
|
||||
const [client] = window.RobosatsSettings.split('-');
|
||||
this.client = client;
|
||||
systemClient.getItem('settings_mode').then((mode) => {
|
||||
this.mode = mode !== '' ? (mode as 'light' | 'dark') : this.getMode();
|
||||
});
|
||||
|
||||
const stopNotifications = systemClient.getItem('settings_stop_notifications');
|
||||
this.stopNotifications = client === 'mobile' && stopNotifications === 'true';
|
||||
systemClient.getItem('settings_fontsize_basic').then((fontSizeCookie) => {
|
||||
this.fontSize = fontSizeCookie !== '' ? Number(fontSizeCookie) : 14;
|
||||
});
|
||||
|
||||
const useProxy = systemClient.getItem('settings_use_proxy');
|
||||
this.useProxy = client === 'mobile' && useProxy !== 'false';
|
||||
apiClient.useProxy = this.useProxy;
|
||||
websocketClient.useProxy = this.useProxy;
|
||||
systemClient.getItem('settings_light_qr').then((result) => {
|
||||
this.lightQRs = result === 'true';
|
||||
});
|
||||
|
||||
systemClient.getItem('settings_language').then((result) => {
|
||||
this.language =
|
||||
result !== ''
|
||||
? (result as Language)
|
||||
: i18n.resolvedLanguage == null
|
||||
? 'en'
|
||||
: (i18n.resolvedLanguage.substring(0, 2) as Language);
|
||||
});
|
||||
|
||||
systemClient.getItem('settings_connection').then((result) => {
|
||||
this.connection = result && result !== '' ? (result as 'api' | 'nostr') : this.connection;
|
||||
});
|
||||
|
||||
systemClient.getItem('settings_network').then((result) => {
|
||||
this.network = result && result !== '' ? (result as 'mainnet' | 'testnet') : this.network;
|
||||
});
|
||||
|
||||
systemClient.getItem('settings_stop_notifications').then((result) => {
|
||||
this.stopNotifications = client === 'mobile' && result === 'true';
|
||||
});
|
||||
|
||||
systemClient.getItem('settings_use_proxy').then((result) => {
|
||||
this.useProxy = client === 'mobile' && result !== 'false';
|
||||
apiClient.useProxy = this.useProxy;
|
||||
websocketClient.useProxy = this.useProxy;
|
||||
});
|
||||
}
|
||||
|
||||
getMode = (): 'light' | 'dark' => {
|
||||
return window?.matchMedia('(prefers-color-scheme: dark)')?.matches ? 'dark' : 'light';
|
||||
};
|
||||
|
||||
public frontend: 'basic' | 'pro' = 'basic';
|
||||
public mode: 'light' | 'dark' = 'light';
|
||||
public mode: 'light' | 'dark' = this.getMode();
|
||||
public client: 'web' | 'mobile' = 'web';
|
||||
public fontSize: number = 14;
|
||||
public lightQRs: boolean = false;
|
||||
@ -74,8 +86,8 @@ class BaseSettings {
|
||||
public host?: string;
|
||||
public unsafeClient: boolean = false;
|
||||
public selfhostedClient: boolean = false;
|
||||
public useProxy: boolean;
|
||||
public stopNotifications: boolean;
|
||||
public useProxy: boolean = false;
|
||||
public stopNotifications: boolean = false;
|
||||
}
|
||||
|
||||
export default BaseSettings;
|
||||
|
||||
@ -7,6 +7,9 @@ declare global {
|
||||
}
|
||||
|
||||
interface AndroidAppRobosats {
|
||||
getEncryptedStorage: (uuid: string, key: string) => void;
|
||||
setEncryptedStorage: (uuid: string, key: string, value: string) => void;
|
||||
deleteEncryptedStorage: (uuid: string, key: string) => void;
|
||||
generateRoboname: (uuid: string, initialString: string) => void;
|
||||
generateRobohash: (uuid: string, initialString: string) => void;
|
||||
copyToClipboard: (value: string) => void;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { type SystemClient } from '..';
|
||||
import AndroidRobosats from '../../Android';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
class SystemAndroidClient implements SystemClient {
|
||||
constructor() {
|
||||
@ -13,44 +14,30 @@ class SystemAndroidClient implements SystemClient {
|
||||
window.AndroidAppRobosats?.copyToClipboard(value ?? '');
|
||||
};
|
||||
|
||||
// Cookies
|
||||
public getCookie: (key: string) => string = (key) => {
|
||||
let cookieValue = null;
|
||||
if (document?.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
// Does this cookie string begin with the key we want?
|
||||
if (cookie.substring(0, key.length + 1) === key + '=') {
|
||||
cookieValue = decodeURIComponent(cookie.substring(key.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cookieValue ?? '';
|
||||
};
|
||||
|
||||
public setCookie: (key: string, value: string) => void = (key, value) => {
|
||||
document.cookie = `${key}=${value};path=/;SameSite=None;Secure`;
|
||||
};
|
||||
|
||||
public deleteCookie: (key: string) => void = (key) => {
|
||||
document.cookie = `${key}= ;path=/; expires = Thu, 01 Jan 1970 00:00:00 GMT`;
|
||||
};
|
||||
|
||||
// Local storage
|
||||
public getItem: (key: string) => string = (key) => {
|
||||
const value = window.localStorage.getItem(key);
|
||||
return value ?? '';
|
||||
public getItem: (key: string) => Promise<string | undefined> = async (key) => {
|
||||
try {
|
||||
const result = await new Promise<string>((resolve, reject) => {
|
||||
const uuid: string = uuidv4();
|
||||
window.AndroidAppRobosats?.getEncryptedStorage(uuid, key);
|
||||
window.AndroidRobosats?.storePromise(uuid, resolve, reject);
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error generating roboname:', error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
public setItem: (key: string, value: string) => void = (key, value) => {
|
||||
window.localStorage.setItem(key, value);
|
||||
const uuid: string = uuidv4();
|
||||
window.AndroidAppRobosats?.setEncryptedStorage(uuid, key, value);
|
||||
};
|
||||
|
||||
public deleteItem: (key: string) => void = (key) => {
|
||||
window.localStorage.removeItem(key);
|
||||
const uuid: string = uuidv4();
|
||||
window.AndroidAppRobosats?.deleteEncryptedStorage(uuid, key);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -26,34 +26,8 @@ class SystemDesktopClient implements SystemClient {
|
||||
}
|
||||
};
|
||||
|
||||
// Cookies
|
||||
public getCookie: (key: string) => string = (key) => {
|
||||
let cookieValue = null;
|
||||
if (document?.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
// Does this cookie string begin with the key we want?
|
||||
if (cookie.substring(0, key.length + 1) === key + '=') {
|
||||
cookieValue = decodeURIComponent(cookie.substring(key.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cookieValue ?? '';
|
||||
};
|
||||
|
||||
public setCookie: (key: string, value: string) => void = (key, value) => {
|
||||
document.cookie = `${key}=${value};path=/;SameSite=None;Secure`;
|
||||
};
|
||||
|
||||
public deleteCookie: (key: string) => void = (key) => {
|
||||
document.cookie = `${key}= ;path=/; expires = Thu, 01 Jan 1970 00:00:00 GMT`;
|
||||
};
|
||||
|
||||
// Local storage
|
||||
public getItem: (key: string) => string = (key) => {
|
||||
public getItem: (key: string) => Promise<string | undefined> = async (key) => {
|
||||
const value = window.sessionStorage.getItem(key);
|
||||
return value ?? '';
|
||||
};
|
||||
|
||||
@ -27,34 +27,8 @@ class SystemWebClient implements SystemClient {
|
||||
}
|
||||
};
|
||||
|
||||
// Cookies
|
||||
public getCookie: (key: string) => string = (key) => {
|
||||
let cookieValue = null;
|
||||
if (document?.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
// Does this cookie string begin with the key we want?
|
||||
if (cookie.substring(0, key.length + 1) === key + '=') {
|
||||
cookieValue = decodeURIComponent(cookie.substring(key.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cookieValue ?? '';
|
||||
};
|
||||
|
||||
public setCookie: (key: string, value: string) => void = (key, value) => {
|
||||
document.cookie = `${key}=${value};path=/;SameSite=None;Secure`;
|
||||
};
|
||||
|
||||
public deleteCookie: (key: string) => void = (key) => {
|
||||
document.cookie = `${key}= ;path=/; expires = Thu, 01 Jan 1970 00:00:00 GMT`;
|
||||
};
|
||||
|
||||
// Local storage
|
||||
public getItem: (key: string) => string = (key) => {
|
||||
public getItem: (key: string) => Promise<string | undefined> = async (key) => {
|
||||
const value = window.localStorage.getItem(key);
|
||||
return value ?? '';
|
||||
};
|
||||
|
||||
@ -5,10 +5,7 @@ import SystemAndroidClient from './SystemAndroidClient';
|
||||
export interface SystemClient {
|
||||
loading: boolean;
|
||||
copyToClipboard: (value: string) => void;
|
||||
getCookie: (key: string) => string | undefined;
|
||||
setCookie: (key: string, value: string) => void;
|
||||
deleteCookie: (key: string) => void;
|
||||
getItem: (key: string) => string | undefined;
|
||||
getItem: (key: string) => Promise<string | undefined>;
|
||||
setItem: (key: string, value: string) => void;
|
||||
deleteItem: (key: string) => void;
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { type ApiClient, type Auth } from '..';
|
||||
import { systemClient } from '../../System';
|
||||
import ApiWebClient from '../ApiWebClient';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@ -32,14 +31,8 @@ class ApiAndroidClient implements ApiClient {
|
||||
return headers;
|
||||
};
|
||||
|
||||
private readonly parseResponse = (response: Record<string, object>): object => {
|
||||
if (response.headers['set-cookie'] != null) {
|
||||
response.headers['set-cookie'].forEach((cookie: string) => {
|
||||
const keySplit: string[] = cookie.split('=');
|
||||
systemClient.setCookie(keySplit[0], keySplit[1].split(';')[0]);
|
||||
});
|
||||
}
|
||||
return response.json;
|
||||
private readonly parseResponse = (response: string): object => {
|
||||
return JSON.parse(response).json;
|
||||
};
|
||||
|
||||
public put: (baseUrl: string, path: string, body: object) => Promise<object | undefined> = async (
|
||||
@ -64,7 +57,7 @@ class ApiAndroidClient implements ApiClient {
|
||||
window.AndroidRobosats?.storePromise(uuid, resolve, reject);
|
||||
});
|
||||
|
||||
return this.parseResponse(JSON.parse(result));
|
||||
return this.parseResponse(result);
|
||||
};
|
||||
|
||||
public post: (
|
||||
@ -84,7 +77,7 @@ class ApiAndroidClient implements ApiClient {
|
||||
window.AndroidRobosats?.storePromise(uuid, resolve, reject);
|
||||
});
|
||||
|
||||
return this.parseResponse(JSON.parse(result));
|
||||
return this.parseResponse(result);
|
||||
};
|
||||
|
||||
public get: (baseUrl: string, path: string, auth?: Auth) => Promise<object | undefined> = async (
|
||||
@ -102,7 +95,7 @@ class ApiAndroidClient implements ApiClient {
|
||||
window.AndroidRobosats?.storePromise(uuid, resolve, reject);
|
||||
});
|
||||
|
||||
return this.parseResponse(JSON.parse(result));
|
||||
return this.parseResponse(result);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,103 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="onion-location" content="{{ ONION_LOCATION }}" />
|
||||
<% 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">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="onion-location" content="{{ ONION_LOCATION }}" />
|
||||
<% 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.">
|
||||
<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"/>
|
||||
<link rel="stylesheet" type="text/css" href="<%= htmlWebpackPlugin.options.basePath %>static/css_pro/react-grid-layout.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="<%= htmlWebpackPlugin.options.basePath %>static/css_pro/react-resizable.css"/>
|
||||
<% } else { %>
|
||||
<title>RoboSats - Simple and Private Bitcoin Exchange</title>
|
||||
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.basePath %>static/css/fonts.css"/>
|
||||
<% } %>
|
||||
<% if (pro) { %>
|
||||
<title>RoboSats PRO - Simple and Private Bitcoin Exchange</title>
|
||||
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.basePath %>static/css_pro/fonts.css" />
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="<%= htmlWebpackPlugin.options.basePath %>static/css_pro/react-grid-layout.css" />
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="<%= htmlWebpackPlugin.options.basePath %>static/css_pro/react-resizable.css" />
|
||||
<% } else { %>
|
||||
<title>RoboSats - Simple and Private Bitcoin Exchange</title>
|
||||
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.basePath %>static/css/fonts.css" />
|
||||
<% } %>
|
||||
|
||||
<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"/>
|
||||
<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 {
|
||||
max-width: 600px;
|
||||
margin: 40px auto;
|
||||
padding: 20px;
|
||||
border: 2px solid #e74c3c;
|
||||
background-color: #fdecea;
|
||||
color: #c0392b;
|
||||
text-align: center;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.noscript-container h2 {
|
||||
margin-top: 0;
|
||||
font-size: 1.8em;
|
||||
}
|
||||
.noscript-container p {
|
||||
font-size: 1.1em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.noscript-container a {
|
||||
color: #c0392b;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- This block is only rendered if JavaScript is disabled -->
|
||||
<noscript>
|
||||
<div class="noscript-container">
|
||||
<h2>JavaScript is Disabled</h2>
|
||||
<p>This website requires JavaScript for full functionality. Please enable JavaScript in your browser settings to proceed.</p>
|
||||
<p>If you’re using Tor Browser, ensure your "Security Level" is set to "Standard". If the issue persists, try clearing your cache and storage.</p>
|
||||
<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>
|
||||
<style>
|
||||
/* Styles for the JavaScript-disabled disclaimer */
|
||||
.noscript-container {
|
||||
max-width: 600px;
|
||||
margin: 40px auto;
|
||||
padding: 20px;
|
||||
border: 2px solid #e74c3c;
|
||||
background-color: #fdecea;
|
||||
color: #c0392b;
|
||||
text-align: center;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
<div id="main">
|
||||
<div id="app">
|
||||
<div class="loaderCenter">
|
||||
<% 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>
|
||||
<% } %>
|
||||
.noscript-container h2 {
|
||||
margin-top: 0;
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
.noscript-container p {
|
||||
font-size: 1.1em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.noscript-container a {
|
||||
color: #c0392b;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- This block is only rendered if JavaScript is disabled -->
|
||||
<noscript>
|
||||
<div class="noscript-container">
|
||||
<h2>JavaScript is Disabled</h2>
|
||||
<p>This website requires JavaScript for full functionality. Please enable JavaScript in your browser settings to
|
||||
proceed.</p>
|
||||
<p>If you’re using Tor Browser, ensure your "Security Level" is set to "Standard". If the issue persists, try
|
||||
clearing your cache and storage.</p>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.RobosatsSettings = '<%= htmlWebpackPlugin.options.robosatsSettings %>';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<script>
|
||||
window.RobosatsSettings = '<%= htmlWebpackPlugin.options.robosatsSettings %>';
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user