mirror of
https://github.com/RoboSats/robosats.git
synced 2025-07-24 18:53:28 +00:00
TS <> Korlin communication
This commit is contained in:
14
frontend/package-lock.json
generated
14
frontend/package-lock.json
generated
@ -51,6 +51,7 @@
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"robo-identities-wasm": "^0.1.0",
|
||||
"simple-plist": "^1.3.1",
|
||||
"uuid": "^11.1.0",
|
||||
"webln": "^0.3.2",
|
||||
"websocket": "^1.0.35"
|
||||
},
|
||||
@ -17013,6 +17014,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
|
@ -98,6 +98,7 @@
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"robo-identities-wasm": "^0.1.0",
|
||||
"simple-plist": "^1.3.1",
|
||||
"uuid": "^11.1.0",
|
||||
"webln": "^0.3.2",
|
||||
"websocket": "^1.0.35"
|
||||
}
|
||||
|
28
frontend/src/services/Android/index.ts
Normal file
28
frontend/src/services/Android/index.ts
Normal file
@ -0,0 +1,28 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
AndroidAppRobosats?: AndroidAppRobosats;
|
||||
AndroidRobosats?: AndroidRobosats;
|
||||
RobosatsSettings: 'web-basic' | 'web-pro' | 'selfhosted-basic' | 'selfhosted-pro';
|
||||
}
|
||||
}
|
||||
|
||||
interface AndroidAppRobosats {
|
||||
generateRoboname: (uuid: string, initialString: string) => void;
|
||||
}
|
||||
|
||||
class AndroidRobosats {
|
||||
private promises: Record<string, (value: string | PromiseLike<string>) => void> = {};
|
||||
|
||||
public storePromise: (
|
||||
uuid: string,
|
||||
promise: (value: string | PromiseLike<string>) => void,
|
||||
) => void = (uuid, promise) => {
|
||||
this.promises[uuid] = promise;
|
||||
};
|
||||
|
||||
public onResolvePromise: (uuid: string, response: string) => void = (uuid, respone) => {
|
||||
this.promises[uuid](respone);
|
||||
};
|
||||
}
|
||||
|
||||
export default AndroidRobosats;
|
4
frontend/src/services/Roboidentities/Android.ts
Normal file
4
frontend/src/services/Roboidentities/Android.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import RoboidentitiesAndroidClient from './RoboidentitiesAndroidClient';
|
||||
import { type RoboidentitiesClient } from './type';
|
||||
|
||||
export const roboidentitiesClient: RoboidentitiesClient = new RoboidentitiesAndroidClient();
|
@ -0,0 +1,44 @@
|
||||
import { type RoboidentitiesClient } from '../type';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
class RoboidentitiesAndroidClient 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 result = await new Promise<string>((resolve) => {
|
||||
const uuid: string = uuidv4();
|
||||
window.AndroidAppRobosats?.generateRoboname(uuid, initialString);
|
||||
window.AndroidRobosats?.storePromise(uuid, resolve);
|
||||
});
|
||||
|
||||
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 RoboidentitiesAndroidClient;
|
55
frontend/src/services/System/SystemAndroidClient/index.ts
Normal file
55
frontend/src/services/System/SystemAndroidClient/index.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { type SystemClient } from '..';
|
||||
import AndroidRobosats from '../../Android';
|
||||
|
||||
class SystemAndroidClient implements SystemClient {
|
||||
constructor() {
|
||||
window.AndroidRobosats = new AndroidRobosats();
|
||||
}
|
||||
|
||||
public loading = false;
|
||||
|
||||
// TODO
|
||||
public copyToClipboard: (value: string) => void = () => {};
|
||||
|
||||
// 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 setItem: (key: string, value: string) => void = (key, value) => {
|
||||
window.localStorage.setItem(key, value);
|
||||
};
|
||||
|
||||
public deleteItem: (key: string) => void = (key) => {
|
||||
window.localStorage.removeItem(key);
|
||||
};
|
||||
}
|
||||
|
||||
export default SystemAndroidClient;
|
@ -1,6 +1,7 @@
|
||||
import SystemNativeClient from './SystemNativeClient';
|
||||
import SystemWebClient from './SystemWebClient';
|
||||
import SystemDesktopClient from './SystemDesktopClient';
|
||||
import SystemAndroidClient from './SystemAndroidClient';
|
||||
|
||||
export interface SystemClient {
|
||||
loading: boolean;
|
||||
@ -21,6 +22,9 @@ function getSystemClient(): SystemClient {
|
||||
} else 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')) {
|
||||
// If userAgent has "AndroidRobosats", we assume the app is running inside the Kotlin webview of the RoboSats Android app.
|
||||
return new SystemAndroidClient();
|
||||
} else {
|
||||
// Otherwise, we assume the app is running in a web browser.
|
||||
return new SystemWebClient();
|
||||
|
@ -152,7 +152,7 @@ const configNode: Configuration = {
|
||||
],
|
||||
};
|
||||
|
||||
const configMobile: Configuration = {
|
||||
const configNative: Configuration = {
|
||||
...config,
|
||||
module: {
|
||||
...config.module,
|
||||
@ -163,7 +163,7 @@ const configMobile: Configuration = {
|
||||
loader: 'file-replace-loader',
|
||||
options: {
|
||||
condition: 'if-replacement-exists',
|
||||
replacement: path.resolve(__dirname, 'src/i18n/Native.js'),
|
||||
replacement: path.resolve(__dirname, 'src/i18n/Mobile.js'),
|
||||
async: true,
|
||||
},
|
||||
},
|
||||
@ -172,7 +172,7 @@ const configMobile: Configuration = {
|
||||
loader: 'file-replace-loader',
|
||||
options: {
|
||||
condition: 'if-replacement-exists',
|
||||
replacement: path.resolve(__dirname, 'src/geo/Native.js'),
|
||||
replacement: path.resolve(__dirname, 'src/geo/Mobile.js'),
|
||||
async: true,
|
||||
},
|
||||
},
|
||||
@ -236,6 +236,63 @@ const configMobile: Configuration = {
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
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, '../mobile_new/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: {
|
||||
@ -293,4 +350,4 @@ const configMobile: Configuration = {
|
||||
],
|
||||
};
|
||||
|
||||
export default [configNode, configMobile];
|
||||
export default [configNode, configNative, configAndroid];
|
||||
|
@ -6,6 +6,7 @@ import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.webkit.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.koalasat.robosats.tor.WebAppInterface
|
||||
import com.robosats.tor.TorKmp
|
||||
import com.robosats.tor.TorKmpManager
|
||||
import java.net.InetSocketAddress
|
||||
@ -394,6 +395,11 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
webView.settings.userAgentString = "AndroidRobosats"
|
||||
|
||||
// Add the JavaScript interface
|
||||
webView.addJavascriptInterface(WebAppInterface(this, webView), "AndroidAppRobosats")
|
||||
|
||||
// Now it's safe to load the local HTML file
|
||||
webView.loadUrl("file:///android_asset/index.html")
|
||||
}
|
||||
|
@ -0,0 +1,18 @@
|
||||
package com.koalasat.robosats.tor
|
||||
|
||||
import android.content.Context
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
|
||||
class WebAppInterface(private val context: Context, private val webView: WebView) {
|
||||
@JavascriptInterface
|
||||
fun generateRoboname(uuid: String, message: String) {
|
||||
// Handle the message received from JavaScript
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||
|
||||
webView.post {
|
||||
webView.evaluateJavascript("javascript:window.AndroidRobosats.onResolvePromise('${uuid}', '${message}')", null)
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user