Skip to content

Commit

Permalink
WIP Browser
Browse files Browse the repository at this point in the history
  • Loading branch information
dguenther committed Sep 25, 2024
1 parent b38f1c7 commit 01d98ce
Show file tree
Hide file tree
Showing 4 changed files with 303 additions and 1 deletion.
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

276 changes: 276 additions & 0 deletions packages/mobile-app/app/menu/debug/browser.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
import { WebView } from "react-native-webview";
import Constants from "expo-constants";
import {
Button,
Modal,
SafeAreaView,
StyleSheet,
Text,
View,
} from "react-native";
import { wallet } from "../../../data/wallet/wallet";
import { Network } from "../../../data/constants";
import { useRef, useState } from "react";
import * as Uint8ArrayUtils from "../../../utils/uint8Array";
import { useFacade } from "../../../data/facades";

type Message = {
id: number;
type: string;
data: Record<string, unknown> | null;
};

class MessageHandler {
activeAccount: { name: string; address: string } | null = null;
activeAccountName = null;
connectRequest: {
resolve: (address: string | null) => void;
reject: () => void;
} | null = null;

async updateActiveAccount(
activeAccount: { name: string; address: string } | null,
) {
this.activeAccount = activeAccount;
if (this.connectRequest) {
this.connectRequest.resolve(activeAccount?.address ?? null);
this.connectRequest = null;
}
}

async handleMessage(
data: string,
showAccountModal: () => void,
postMessage?: (data: string) => void,
) {
console.log(data);
let message: Message;
try {
message = JSON.parse(data);
if (
typeof message.id !== "number" ||
typeof message.type !== "string" ||
typeof message.data !== "object"
) {
throw new Error("Invalid message");
}
} catch (e) {
console.error(`Invalid message: ${data}`);
return;
}

if (message.type === "connect") {
showAccountModal();
const address = await new Promise<string | null>((resolve, reject) => {
this.connectRequest = { resolve, reject };
});
postMessage?.(
JSON.stringify({
id: message.id,
type: "connect",
data: { address },
}),
);
} else if (message.type === "getBalances") {
if (wallet.state.type !== "STARTED" || !this.activeAccount) {
console.error("Wallet not started");
return;
}
const data = await wallet.getAccountWithHeadAndBalances(
Network.TESTNET,
this.activeAccount.name,
);
if (!data) {
console.error(`No account found with name ${this.activeAccount.name}`);
return;
}

const newBalances = await Promise.all(
data.balances.map(async (b) => {
const hexId = Uint8ArrayUtils.toHex(b.assetId);
const asset = await wallet.getAsset(Network.TESTNET, hexId);
return {
assetId: hexId,
balance: b.confirmed,
assetName: asset?.name ?? hexId,
};
}),
);

postMessage?.(
JSON.stringify({
id: message.id,
type: "getBalances",
data: {
balances: newBalances,
},
}),
);
} else {
console.error(`Invalid message type: ${message.type}`);
}
}
}

export default function MenuDebugBrowser() {
const webref = useRef<WebView | null>(null);
const messageHandler = useRef(new MessageHandler());
const facade = useFacade();

const [modalVisible, setModalVisible] = useState(false);
const accounts = facade.getAccounts.useQuery(undefined, {
enabled: modalVisible,
});

const js = `
window.addEventListener('message', (event) => {
console.log(event.data);
let message;
try {
message = JSON.parse(event.data);
if (typeof message.id !== "number" || typeof message.type !== "string" || typeof message.data !== "object") {
throw new Error("Invalid message");
}
} catch (e) {
console.error(\`Invalid message: $\{event.data\}\`);
return;
}
if (message.type === "connect") {
window.rpccalls[message.id].resolve(message.data.address);
} else if (message.type === "getBalances") {
window.rpccalls[message.id].resolve(message.data.balances);
}
});
window.rpccounter = 0;
window.rpccalls = {};
class IronFishBridge {
#address;
constructor() {
this.#address = null;
}
get address() {
return this.#address;
}
async connect() {
const id = window.rpccounter++;
window.ReactNativeWebView.postMessage(JSON.stringify({
id,
type: "connect",
data: null,
}));
const result = await new Promise((resolve, reject) => {
window.rpccalls[id] = { resolve, reject };
});
if (result == null) {
throw new Error("No account selected");
}
this.#address = result;
console.log(result);
return result;
}
async getBalances() {
if (!this.#address) {
throw new Error("Connect first");
}
const id = window.rpccounter++;
window.ReactNativeWebView.postMessage(JSON.stringify({
id,
type: "getBalances",
data: null,
}));
const result = await new Promise((resolve, reject) => {
window.rpccalls[id] = { resolve, reject };
});
console.log(result);
return result;
}
}
window.ironfish = new Proxy(new IronFishBridge(), {
get: (obj, property, receiver) => {
if (!property in obj) {
const message = \`ERROR: Please implement $\{property\} in IronFishBridge\`;
console.error(message);
return;
}
const val = obj[property];
if (val instanceof Function) {
return function (...args) {
return val.apply(this === receiver ? obj : this, args);
};
} else {
return val;
}
},
});
`;

return (
<View style={styles.container}>
<Modal
animationType="slide"
visible={modalVisible}
onRequestClose={() => {
setModalVisible(false);
}}
>
<SafeAreaView>
<View style={{ paddingTop: 40, paddingHorizontal: 4 }}>
<Text style={{ fontSize: 20, textAlign: "center" }}>
This website would like to connect to your wallet. Choose an
account to connect, or click Cancel.
</Text>
<Text style={{ textAlign: "center" }}>
Choose an account to connect, or click Cancel.
</Text>
{accounts.data?.map((a) => (
<Button
key={a.name}
onPress={() => {
messageHandler.current.updateActiveAccount({
name: a.name,
address: a.publicAddress,
});
setModalVisible(false);
}}
title={`${a.name} (${a.balances.iron.confirmed} $IRON)`}
/>
))}
<Button
onPress={() => {
messageHandler.current.updateActiveAccount(null);
setModalVisible(false);
}}
title="Cancel"
/>
</View>
</SafeAreaView>
</Modal>
<WebView
source={{ uri: "https://testnet.bridge.ironfish.network" }}
ref={(r) => (webref.current = r)}
injectedJavaScriptBeforeContentLoaded={js}
onMessage={(event) => {
messageHandler.current.handleMessage(
event.nativeEvent.data,
() => {
setModalVisible(true);
},
webref.current?.postMessage,
);
}}
webviewDebuggingEnabled
/>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: Constants.statusBarHeight,
},
});
1 change: 1 addition & 0 deletions packages/mobile-app/app/menu/debug/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default function MenuDebug() {
<View style={styles.container}>
<LinkButton title="Pending Transactions" href="/menu/debug/pending/" />
<LinkButton title="Unspent Notes" href="/menu/debug/unspent/" />
<LinkButton title="Browser" href="/menu/debug/browser/" />
<View>
{walletStatus.data && (
<>
Expand Down
3 changes: 2 additions & 1 deletion packages/mobile-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"react-native-safe-area-context": "4.10.8",
"react-native-screens": "3.32.0",
"react-native-svg": "15.2.0",
"zod": "^3.22.4"
"zod": "^3.22.4",
"react-native-webview": "13.8.6"
},
"devDependencies": {
"@babel/core": "^7.20.0",
Expand Down

0 comments on commit 01d98ce

Please sign in to comment.