TS <> Korlin communication

This commit is contained in:
koalasat
2025-07-16 11:32:52 +02:00
parent a46047e28b
commit 41f1b3a5f2
12 changed files with 235 additions and 4 deletions

View File

@ -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",

View File

@ -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"
}

View 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;

View File

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

View File

@ -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;

View 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;

View File

@ -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();

View File

@ -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];

View File

@ -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")
}

View File

@ -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)
}
}
}