diff --git a/README.md b/README.md index 833bf37..9ab973a 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ Targets can be defined in the `package.json` or `projects.json`. Learn more [in ### New Build +- Update the Version and Build numbers in `app.json`. If you're pushing an update, you'll probably just want to increment the build. + ```shell nx prebuild mobile-app -- --platform ios nx cargo-ios ironfish-native-module -- --target='ios' @@ -61,7 +63,8 @@ open . - Double Click "mobileapp" - Signing & Capabilities tab - Under signing select "IF Labs" for team (must be added to team in App Store Connect, ask Derek) -- Bundle identifier should be prepopulated but should read "com.ironfish.mobileapp" +- Bundle identifier should be prepopulated but should read "network.ironfish.mobilewallet" +- Double-check Version and Build numbers match those in `app.json`. - In the scheme bar (top center of editor), select Any iOS Device (arm64) - Mac menu bar - click Product -> Archive, wait for build (might take a minute or two) - Click Distribute App button in popup diff --git a/packages/mobile-app/app.json b/packages/mobile-app/app.json index 98bd8bf..ab3b0eb 100644 --- a/packages/mobile-app/app.json +++ b/packages/mobile-app/app.json @@ -16,6 +16,8 @@ "ios": { "icon": "./assets/icon.png", "bundleIdentifier": "network.ironfish.mobilewallet", + "appleTeamId": "9WR79D873L", + "buildNumber": "2", "infoPlist": { "NSAppTransportSecurity": { "NSAllowsArbitraryLoads": false, diff --git a/packages/mobile-app/app/(drawer)/account/bridge/index.tsx b/packages/mobile-app/app/(drawer)/account/bridge/index.tsx index 05c2bd2..eecc6f3 100644 --- a/packages/mobile-app/app/(drawer)/account/bridge/index.tsx +++ b/packages/mobile-app/app/(drawer)/account/bridge/index.tsx @@ -3,7 +3,7 @@ import { StyleSheet, View } from "react-native"; import { Button, Card, Layout, Text } from "@ui-kitten/components"; import { oreoWallet } from "@/data/wallet/oreowalletWallet"; import { Network } from "@/data/constants"; -import { useCallback, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import * as Uint8ArrayUtils from "@/utils/uint8Array"; import { useFacade } from "@/data/facades"; import { Output } from "@/data/facades/wallet/types"; @@ -219,15 +219,13 @@ export default function MenuDebugBrowser() { }, ); - const handlePresentModalPress = useCallback(() => { - bottomSheetModalRef.current?.present(); - setAccountModalVisible(true); - }, []); - - const handleDismissModalPress = useCallback(() => { - bottomSheetModalRef.current?.dismiss(); - setAccountModalVisible(false); - }, []); + useEffect(() => { + if (accountModalVisible) { + bottomSheetModalRef.current?.present(); + } else { + bottomSheetModalRef.current?.dismiss(); + } + }, [accountModalVisible]); const renderBackdrop = useCallback( (props: any) => ( @@ -379,7 +377,10 @@ export default function MenuDebugBrowser() { enablePanDownToClose backdropComponent={renderBackdrop} onDismiss={() => { - messageHandler.current.updateActiveAccount(null); + if (messageHandler.current.connectRequest) { + messageHandler.current.connectRequest.resolve(null); + messageHandler.current.connectRequest = null; + } setAccountModalVisible(false); }} backgroundStyle={styles.bottomSheetModal} @@ -391,7 +392,7 @@ export default function MenuDebugBrowser() { style={{ width: 48, height: 48 }} /> - Iron Fish Bridge + Connect Account {network} @@ -412,34 +413,36 @@ export default function MenuDebugBrowser() { > + - { messageHandler.current.rejectSendTransactionRequest(); @@ -457,7 +460,7 @@ export default function MenuDebugBrowser() { onMessage={(event) => { messageHandler.current.handleMessage( event.nativeEvent.data, - handlePresentModalPress, + () => setAccountModalVisible(true), (data) => { setSendTransactionData(data); }, diff --git a/packages/mobile-app/app/(drawer)/account/transaction/[hash].tsx b/packages/mobile-app/app/(drawer)/account/transaction/[hash].tsx index 57a3328..4050c5c 100644 --- a/packages/mobile-app/app/(drawer)/account/transaction/[hash].tsx +++ b/packages/mobile-app/app/(drawer)/account/transaction/[hash].tsx @@ -65,6 +65,16 @@ export default function TransactionDetails() { }, ); + const explorer = facade.getExplorerUrl.useQuery( + { + type: "transaction", + hash: hash, + }, + { + enabled: !!hash, + }, + ); + // Get assets for all balance deltas const assetQueries = useQueries({ queries: @@ -76,7 +86,8 @@ export default function TransactionDetails() { }); const openInExplorer = () => { - Linking.openURL(`https://explorer.ironfish.network/transaction/${hash}`); + if (!explorer.data) return; + Linking.openURL(explorer.data); }; if (transactionQuery.isLoading || assetQueries.some((q) => q.isLoading)) { diff --git a/packages/mobile-app/components/browser/SendTransactionModal.tsx b/packages/mobile-app/components/browser/SendTransactionModal.tsx index b080a3f..41b4ed9 100644 --- a/packages/mobile-app/components/browser/SendTransactionModal.tsx +++ b/packages/mobile-app/components/browser/SendTransactionModal.tsx @@ -1,9 +1,16 @@ -import { Network } from "@/data/constants"; +import { IRON_ASSET_ID_HEX, Network } from "@/data/constants"; import { Output } from "@/data/facades/wallet/types"; import { oreoWallet } from "@/data/wallet/oreowalletWallet"; -import { Button, Modal, SafeAreaView, Text, View } from "react-native"; -import { useState, useEffect } from "react"; -import { RawTransaction } from "@ironfish/sdk"; +import { StyleSheet, View } from "react-native"; +import { useState, useEffect, useRef } from "react"; +import { CurrencyUtils, RawTransaction } from "@ironfish/sdk"; +import { BottomSheetModal, BottomSheetScrollView } from "@gorhom/bottom-sheet"; +import { Button, Card, Layout, Spinner, Text } from "@ui-kitten/components"; +import { useFacade } from "@/data/facades"; +import { useQueries } from "@tanstack/react-query"; +import { Image } from "expo-image"; +import { Asset } from "@/data/facades/chain/types"; +import { setStringAsync } from "expo-clipboard"; type Mint = { value: string; @@ -31,19 +38,80 @@ type DisplayMessage = | { type: "success"; hash: string; message: string }; export default function SendTransactionModal({ + network, sendTransactionData, + renderBackdrop, cancel, success, }: { + network: string; sendTransactionData: GeneralTransactionData | null; + renderBackdrop: (props: any) => React.JSX.Element; cancel: () => void; success: (hash: string) => void; }) { + const facade = useFacade(); + const bottomSheetModalRef = useRef(null); + const [displayMessage, setDisplayMessage] = useState( null, ); const [rawTxn, setRawTxn] = useState(null); + const assetIds = [ + ...new Set( + rawTxn?.outputs.map((o) => o.note.assetId().toString("hex")) ?? [], + ), + ]; + + const assetQueries = useQueries({ + queries: + assetIds.map((assetId) => ({ + queryKey: facade.getAsset.buildQueryKey({ assetId }), + queryFn: () => facade.getAsset.resolver({ assetId }), + })) ?? [], + }); + + const assetMap = new Map< + string, + { name: string; image?: string; asset: Asset } + >(); + for (const a of assetQueries) { + if (a.data) { + assetMap.set(a.data.id, { + name: + a.data?.verification.status === "verified" + ? a.data.verification.symbol + : a.data.name, + image: + a.data?.verification.status === "verified" + ? a.data.verification.logoURI + : undefined, + asset: a.data, + }); + } + } + + const assetBalanceDeltas = new Map(); + if (rawTxn) { + assetBalanceDeltas.set(IRON_ASSET_ID_HEX, rawTxn?.fee); + for (const o of rawTxn.outputs) { + if (o.note.owner() !== o.note.sender()) { + const assetId = o.note.assetId().toString("hex"); + const currentBalance = assetBalanceDeltas.get(assetId) ?? 0n; + assetBalanceDeltas.set(assetId, currentBalance + o.note.value()); + } + } + } + + useEffect(() => { + if (sendTransactionData) { + bottomSheetModalRef.current?.present(); + } else { + bottomSheetModalRef.current?.dismiss(); + } + }, [sendTransactionData]); + useEffect(() => { let cancel = false; @@ -89,24 +157,68 @@ export default function SendTransactionModal({ const renderBody = () => { if (displayMessage) { if (displayMessage.type === "loading") { - return {displayMessage.message}; + return ( + + + {displayMessage.message} + + + + ); } else if (displayMessage.type === "error") { return ( - <> - {displayMessage.message} - + ); } else if (displayMessage.type === "success") { return ( - <> - {displayMessage.message} - {`Transaction hash: ${displayMessage.hash}`} - + + + ); } } @@ -114,72 +226,167 @@ export default function SendTransactionModal({ if (rawTxn !== null) { return ( <> - - {`From: ${sendTransactionData?.from} (${rawTxn.outputs[0].note.sender()})`} - - Fee: {rawTxn.fee.toString()} - - Outputs + + Account + + + {sendTransactionData?.from} + + + {rawTxn.outputs[0].note.sender()} + + + + + Outputs {rawTxn.outputs?.map((o, i) => { let memo; try { - console.log(o.note.memo()); memo = new TextDecoder().decode(o.note.memo()); } catch { memo = o.note.memo().toString("hex"); } + const asset = assetMap.get(o.note.assetId().toString("hex")); return ( - - Output {i + 1} - - To: {o.note.owner()} - Amount: {o.note.value().toString()} - Asset: {o.note.assetId().toString("hex")} - Memo: {memo} - - + + + + + + + + + {asset?.name ?? o.note.assetId().toString("hex")} + + + {CurrencyUtils.render( + o.note.value(), + false, + o.note.assetId().toString("hex"), + asset?.asset.verification.status === "verified" + ? asset.asset.verification + : undefined, + )} + + + + {`To: ${o.note.owner()}`} + + + {`Memo: ${memo}`} + + + + ); })} - + + ); } @@ -188,18 +395,65 @@ export default function SendTransactionModal({ }; return ( - success(displayMessage.hash) + : cancel + } + backgroundStyle={styles.bottomSheetModal} > - - - Send Transaction - This website would like to send a transaction. - {renderBody()} - - - + + + + + Send Transaction + + {network} + + + + {renderBody()} + + ); } + +const styles = StyleSheet.create({ + assetBadge: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: "#E1E1E1", + elevation: 4, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 4, + }, + bottomSheetModal: { + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 6, + }, + shadowOpacity: 0.37, + shadowRadius: 7.49, + + elevation: 12, + }, + bottomSheetContent: { + padding: 16, + gap: 16, + }, + modalSubtitle: { + textAlign: "center", + }, +}); diff --git a/packages/mobile-app/data/wallet/oreowalletWallet.ts b/packages/mobile-app/data/wallet/oreowalletWallet.ts index 98fc62a..fe2263b 100644 --- a/packages/mobile-app/data/wallet/oreowalletWallet.ts +++ b/packages/mobile-app/data/wallet/oreowalletWallet.ts @@ -393,14 +393,12 @@ export class Wallet { memo = memo.padEnd(64, "0"); const key = `${output.publicAddress}:${output.amount}:${output.assetId ?? IRON_ASSET_ID_HEX}:${memo}`; - console.log(`Output key: ${key}`); map.set(key, (map.get(key) ?? 0) + 1); } for (const output of rawTransaction.outputs) { const key = `${output.note.owner()}:${output.note.value()}:${output.note.assetId().toString("hex")}:${output.note.memo().toString("hex")}`; - console.log(`rawTxn output key: ${key}`); const count = map.get(key) ?? 0;