feat: NWA + NWC deep linking (#272)
* feat: nwc deep linking * chore: remove permission component * chore: remove comment * chore: add plug connection animation * feat: add NWA (WIP) * fix: nwa schemes in app config * feat: add missing nwa fields * chore: split components * fix: return to home * fix: redirect countdown * fix: do not allow exporting NWC connection with create_connection capability * chore: rename variable * chore: bump js sdk version * chore: add nwc icon and default app icon * chore: remove unnecessary call to get_info in connect wallet screen * fix: use push instead of replace after dismissing * chore: change wording * fix: plug animation * chore: unify modals (#290) * chore: reduce redirect countdown from 5 to 3 seconds * chore: bump js-sdk to 4.1.0 * chore: use encryptions instead of versions --------- Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
This commit is contained in:
co-authored by
Roland Bewick
parent
ef21b5dc65
commit
3c557652ad
+10
-1
@@ -7,7 +7,16 @@ export default ({ config }) => {
|
||||
name: "Alby Go",
|
||||
slug: "alby-mobile",
|
||||
version: "1.10.0",
|
||||
scheme: ["lightning", "bitcoin", "alby", "nostr+walletconnect"],
|
||||
scheme: [
|
||||
"lightning",
|
||||
"bitcoin",
|
||||
"alby",
|
||||
"nostr+walletconnect",
|
||||
"nostrnwc",
|
||||
"nostrnwc+alby",
|
||||
"nostr+walletauth",
|
||||
"nostr+walletauth+alby",
|
||||
],
|
||||
orientation: "portrait",
|
||||
icon: "./assets/icon.png",
|
||||
userInterfaceStyle: "automatic",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ConnectWallet } from "../../../../pages/settings/wallets/ConnectWallet";
|
||||
|
||||
export default function Page() {
|
||||
return <ConnectWallet />;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 545 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
+31
-23
@@ -23,33 +23,41 @@ function BTCMapModal({ visible, onClose }: BTCMapModalProps) {
|
||||
onPress={onClose}
|
||||
className="absolute inset-0"
|
||||
/>
|
||||
<View className="flex flex-col items-center w-4/5 max-w-[425px] bg-background border border-border p-6 rounded-2xl z-10">
|
||||
<TouchableOpacity onPress={onClose} className="absolute right-0 p-2">
|
||||
<XIcon className="text-muted-foreground" width={32} height={32} />
|
||||
</TouchableOpacity>
|
||||
<Image
|
||||
source={require("./../assets/btc-map.png")}
|
||||
className="w-24 h-24"
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<View className="mt-8 flex flex-col items-center gap-4">
|
||||
<View className="w-4/5 max-w-[425px] bg-background border border-border rounded-2xl z-10">
|
||||
<View className="flex-row items-center justify-center relative p-6">
|
||||
<Text className="text-3xl font-semibold2 text-muted-foreground">
|
||||
BTC Map
|
||||
</Text>
|
||||
<Text className="text-lg text-center text-muted-foreground">
|
||||
BTC Map is an open-source project with the goal of mapping and
|
||||
maintaining all the merchants accepting Bitcoin around the world.
|
||||
</Text>
|
||||
<Text className="text-lg text-center text-muted-foreground">
|
||||
Find merchants nearby, pay for goods and services, and help
|
||||
improve the map by contributing!
|
||||
</Text>
|
||||
<Text
|
||||
onPress={() => openURL("https://btcmap.org/")}
|
||||
className="text-lg underline font-semibold2 text-muted-foreground"
|
||||
<TouchableOpacity
|
||||
onPress={onClose}
|
||||
className="absolute right-0 p-4"
|
||||
>
|
||||
Visit btcmap.org
|
||||
</Text>
|
||||
<XIcon className="text-muted-foreground" width={24} height={24} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View className="p-6 pt-0 flex flex-col items-center">
|
||||
<Image
|
||||
source={require("./../assets/btc-map.png")}
|
||||
className="w-24 h-24"
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<View className="mt-6 flex flex-col items-center gap-4">
|
||||
<Text className="text-lg text-center text-muted-foreground">
|
||||
BTC Map is an open-source project with the goal of mapping and
|
||||
maintaining all the merchants accepting Bitcoin around the
|
||||
world.
|
||||
</Text>
|
||||
<Text className="text-lg text-center text-muted-foreground">
|
||||
Find merchants nearby, pay for goods and services, and help
|
||||
improve the map by contributing!
|
||||
</Text>
|
||||
<Text
|
||||
onPress={() => openURL("https://btcmap.org/")}
|
||||
className="text-lg underline font-semibold2 text-muted-foreground"
|
||||
>
|
||||
Visit btcmap.org
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
+30
-17
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { Modal, TouchableOpacity, View } from "react-native";
|
||||
import { XIcon } from "~/components/Icons";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Text } from "~/components/ui/text";
|
||||
|
||||
@@ -22,25 +23,37 @@ function HelpModal({ visible, onClose }: HelpModalProps) {
|
||||
onPress={onClose}
|
||||
className="absolute inset-0"
|
||||
/>
|
||||
<View className="w-4/5 max-w-[425px] bg-background border border-border p-6 rounded-2xl z-10">
|
||||
<Text className="text-xl text-foreground font-bold2 mb-2">
|
||||
Connect Your Wallet
|
||||
</Text>
|
||||
<View className="flex flex-col mb-4">
|
||||
<Text className="text-muted-foreground">
|
||||
Follow these steps to connect Alby Go to your Hub:
|
||||
</Text>
|
||||
<Text className="text-muted-foreground">1. Open your Alby Hub</Text>
|
||||
<Text className="text-muted-foreground">
|
||||
2. Go to App Store » Alby Go
|
||||
</Text>
|
||||
<Text className="text-muted-foreground">
|
||||
3. Scan the QR code with this app
|
||||
<View className="w-4/5 max-w-[425px] bg-background border border-border rounded-2xl z-10">
|
||||
<View className="flex-row items-center justify-center relative p-6">
|
||||
<Text className="text-xl font-bold2 text-foreground">
|
||||
Connect Your Wallet
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={onClose}
|
||||
className="absolute right-0 p-4"
|
||||
>
|
||||
<XIcon className="text-muted-foreground" width={24} height={24} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View className="p-6 pt-0 flex flex-col">
|
||||
<View className="flex flex-col mb-4">
|
||||
<Text className="text-muted-foreground">
|
||||
Follow these steps to connect Alby Go to your Hub:
|
||||
</Text>
|
||||
<Text className="text-muted-foreground">
|
||||
1. Open your Alby Hub
|
||||
</Text>
|
||||
<Text className="text-muted-foreground">
|
||||
2. Go to App Store » Alby Go
|
||||
</Text>
|
||||
<Text className="text-muted-foreground">
|
||||
3. Scan the QR code with this app
|
||||
</Text>
|
||||
</View>
|
||||
<Button onPress={onClose}>
|
||||
<Text className="font-bold2">OK</Text>
|
||||
</Button>
|
||||
</View>
|
||||
<Button onPress={onClose}>
|
||||
<Text className="font-bold2">OK</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
PopiconsAddressBookSolid as BookUserIcon,
|
||||
PopiconsCameraWebOffSolid as CameraOffIcon,
|
||||
PopiconsCircleCheckLine as CheckCircleIcon,
|
||||
PopiconsChevronRightSolid as ChevronRightIcon,
|
||||
PopiconsChevronTopLine as ChevronUpIcon,
|
||||
PopiconsCopySolid as CopyIcon,
|
||||
PopiconsEditSolid as EditIcon,
|
||||
@@ -56,6 +57,7 @@ interopIcon(BookUserIcon);
|
||||
interopIcon(CameraOffIcon);
|
||||
interopIcon(CheckCircleIcon);
|
||||
interopIcon(ChevronUpIcon);
|
||||
interopIcon(ChevronRightIcon);
|
||||
interopIcon(CopyIcon);
|
||||
interopIcon(EditIcon);
|
||||
interopIcon(ExportIcon);
|
||||
@@ -91,6 +93,7 @@ export {
|
||||
BookUserIcon,
|
||||
CameraOffIcon,
|
||||
CheckCircleIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronUpIcon,
|
||||
CopyIcon,
|
||||
EditIcon,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
import { useColorScheme } from "react-native";
|
||||
import Svg, { Path, SvgProps } from "react-native-svg";
|
||||
|
||||
const NWCIcon = (props: SvgProps) => {
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
// hex values of text-muted-foreground
|
||||
const colors = {
|
||||
light: "#71717A",
|
||||
dark: "#A1A1AA",
|
||||
};
|
||||
|
||||
const color = colorScheme === "dark" ? colors.dark : colors.light;
|
||||
|
||||
return (
|
||||
<Svg width={24} height={24} viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<Path
|
||||
fill={color}
|
||||
d="M10.3692 2.82069C9.40982 1.86492 7.85171 1.87515 6.89571 2.8343L0.710663 9.04102C-0.245454 10.0003 -0.233289 11.531 0.725862 12.4869L10.8668 22.6279C11.8262 23.5835 13.3812 23.5756 14.3372 22.6164L17.3515 19.6022C16.4723 20.4813 15.7489 19.8491 14.8917 18.9955L13.2104 17.3143C11.9341 17.8195 10.4424 17.5685 9.4083 16.5364L8.1271 15.2552C8.084 15.2125 8.05074 15.1679 8.02736 15.1118C8.00399 15.0558 7.99193 14.9958 7.99188 14.9351C7.99183 14.8744 8.00378 14.8143 8.02706 14.7582C8.05034 14.7022 8.08681 14.6547 8.12984 14.6119L8.85486 13.8869L7.27657 12.3086C7.02951 12.062 6.98916 11.6639 7.2101 11.3949C7.46264 11.0861 7.92054 11.0697 8.1976 11.3462L9.79724 12.9445L10.8807 11.861L9.29866 10.2829C9.05149 10.0362 9.01126 9.63821 9.23407 9.36736C9.29317 9.29542 9.36667 9.23663 9.44985 9.19478C9.53302 9.15292 9.62403 9.12893 9.71703 9.12433C9.81002 9.11974 9.90295 9.13465 9.98984 9.16811C10.0767 9.20156 10.1557 9.25282 10.2216 9.3186L11.8277 10.914L12.5084 10.2334C12.5511 10.1903 12.5932 10.1515 12.6492 10.1282C12.7052 10.1048 12.7653 10.0927 12.826 10.0927C12.8867 10.0926 12.9468 10.1045 13.0029 10.1278C13.0589 10.151 13.1098 10.1851 13.1527 10.2281L14.4359 11.5088C15.4571 12.528 15.724 14.0113 15.2466 15.2781L16.9282 16.9597C17.7855 17.8133 18.5028 18.4508 19.3819 17.5717L23.1855 13.7681C22.2713 14.6823 21.5202 13.978 20.6166 13.0753L10.3692 2.82069Z"
|
||||
/>
|
||||
<Path
|
||||
fill={color}
|
||||
d="M17.5603 1.12951L14.3271 4.36014L21.511 11.544C22.1987 12.2287 22.7963 12.7744 23.4595 12.5098C23.8429 12.3569 24.106 11.9411 23.9591 11.5555C20.5215 2.53557 20.5201 2.53543 20.0641 1.54948C19.6081 0.563529 18.3257 0.354468 17.5603 1.12951Z"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default NWCIcon;
|
||||
+58
@@ -1,3 +1,5 @@
|
||||
import { nwa } from "@getalby/sdk";
|
||||
import { NWAOptions } from "@getalby/sdk/dist/NWAClient";
|
||||
import { router } from "expo-router";
|
||||
import { BOLT11_REGEX } from "./constants";
|
||||
import { lnurl as lnurlLib } from "./lnurl";
|
||||
@@ -7,6 +9,10 @@ const SUPPORTED_SCHEMES = [
|
||||
"bitcoin:",
|
||||
"alby:",
|
||||
"nostr+walletconnect:",
|
||||
"nostrnwc:",
|
||||
"nostrnwc+alby:",
|
||||
"nostr+walletauth:",
|
||||
"nostr+walletauth+alby:",
|
||||
];
|
||||
|
||||
// Register exp scheme for testing during development
|
||||
@@ -17,16 +23,67 @@ if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test") {
|
||||
|
||||
export const handleLink = async (url: string) => {
|
||||
if (!url) {
|
||||
console.error("no url to handle");
|
||||
return;
|
||||
}
|
||||
console.info("handling link", url);
|
||||
|
||||
const parsedUrl = new URL(url);
|
||||
if (!parsedUrl.protocol) {
|
||||
console.error("no protocol in URL", url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (SUPPORTED_SCHEMES.indexOf(parsedUrl.protocol) > -1) {
|
||||
let { username, hostname, protocol, pathname, search } = parsedUrl;
|
||||
if (parsedUrl.protocol.startsWith("nostr+walletauth")) {
|
||||
if (router.canDismiss()) {
|
||||
router.dismissAll();
|
||||
}
|
||||
|
||||
const nwaOptions = nwa.NWAClient.parseWalletAuthUrl(url);
|
||||
|
||||
router.push({
|
||||
pathname: "/settings/wallets/connect",
|
||||
params: {
|
||||
options: JSON.stringify(nwaOptions),
|
||||
flow: "nwa",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedUrl.protocol.startsWith("nostrnwc")) {
|
||||
if (router.canDismiss()) {
|
||||
router.dismissAll();
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(search);
|
||||
const appname = params.get("appname");
|
||||
const rawCallback = params.get("callback");
|
||||
const rawAppIcon = params.get("appicon");
|
||||
if (!appname || !rawCallback || !rawAppIcon) {
|
||||
return;
|
||||
}
|
||||
|
||||
const appicon = decodeURIComponent(rawAppIcon);
|
||||
const callback = decodeURIComponent(rawCallback);
|
||||
|
||||
console.info("Navigating to NWA flow");
|
||||
router.push({
|
||||
pathname: "/settings/wallets/connect",
|
||||
params: {
|
||||
options: JSON.stringify({
|
||||
icon: appicon,
|
||||
name: appname,
|
||||
returnTo: callback,
|
||||
} as NWAOptions),
|
||||
flow: "deeplink",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedUrl.protocol === "nostr+walletconnect:") {
|
||||
if (router.canDismiss()) {
|
||||
router.dismissAll();
|
||||
@@ -123,6 +180,7 @@ export const handleLink = async (url: string) => {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error("Unsupported scheme", url);
|
||||
// Redirect the user to the home screen
|
||||
// if no match was found
|
||||
router.replace({
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function registerWalletNotifications(
|
||||
}
|
||||
|
||||
const walletServiceInfo = await nwcClient.getWalletServiceInfo();
|
||||
const isNip44 = walletServiceInfo.versions.includes("1.0");
|
||||
const isNip44 = walletServiceInfo.encryptions.includes("nip44_v2");
|
||||
|
||||
const pushToken = useAppStore.getState().expoPushToken;
|
||||
if (!pushToken) {
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@
|
||||
"@alevy97/react-native-userdefaults": "^0.2.2",
|
||||
"@getalby/expo-shared-preferences": "^0.0.1",
|
||||
"@getalby/lightning-tools": "^5.1.2",
|
||||
"@getalby/sdk": "^3.9.0",
|
||||
"@getalby/sdk": "^4.1.0",
|
||||
"@noble/curves": "^1.6.0",
|
||||
"@popicons/react-native": "^0.0.22",
|
||||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Button } from "~/components/ui/button";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Text } from "~/components/ui/text";
|
||||
import { errorToast } from "~/lib/errorToast";
|
||||
import { handleLink } from "~/lib/link";
|
||||
import { convertMerchantQRToLightningAddress } from "~/lib/merchants";
|
||||
|
||||
export function Send() {
|
||||
@@ -55,6 +56,11 @@ export function Send() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (text.startsWith("nostr+walletauth") /* can have : or +alby: */) {
|
||||
handleLink(text);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Some apps use uppercased LIGHTNING: prefixes
|
||||
text = text.toLowerCase();
|
||||
|
||||
|
||||
@@ -0,0 +1,506 @@
|
||||
import { NWAOptions } from "@getalby/sdk/dist/NWAClient";
|
||||
import { bytesToHex } from "@noble/hashes/utils";
|
||||
import { openURL } from "expo-linking";
|
||||
import { Link, router, useLocalSearchParams } from "expo-router";
|
||||
import { generateSecretKey, getPublicKey } from "nostr-tools";
|
||||
import React from "react";
|
||||
import {
|
||||
Animated,
|
||||
Easing,
|
||||
Image,
|
||||
Modal,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
|
||||
import { Tick } from "~/animations/Tick";
|
||||
import { WalletIcon, XIcon } from "~/components/Icons";
|
||||
import NWCIcon from "~/components/icons/NWCIcon";
|
||||
import Loading from "~/components/Loading";
|
||||
import Screen from "~/components/Screen";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Text } from "~/components/ui/text";
|
||||
import { useGetFiatAmount } from "~/hooks/useGetFiatAmount";
|
||||
import { DEFAULT_WALLET_NAME } from "~/lib/constants";
|
||||
import { errorToast } from "~/lib/errorToast";
|
||||
import { useAppStore } from "~/lib/state/appStore";
|
||||
|
||||
export function ConnectWallet() {
|
||||
let { options: optionsJSON, flow } = useLocalSearchParams<{
|
||||
options: string;
|
||||
flow: "nwa" | "deeplink";
|
||||
}>();
|
||||
const options = JSON.parse(optionsJSON) as Partial<NWAOptions>;
|
||||
const {
|
||||
appPubkey,
|
||||
name,
|
||||
icon,
|
||||
returnTo,
|
||||
notificationTypes,
|
||||
expiresAt,
|
||||
isolated,
|
||||
metadata,
|
||||
} = options;
|
||||
let pubkey = appPubkey;
|
||||
const budgetRenewal = options.budgetRenewal || "monthly";
|
||||
const requestMethods = options.requestMethods || [
|
||||
"get_info",
|
||||
"get_balance",
|
||||
"get_budget",
|
||||
"make_invoice",
|
||||
"pay_invoice",
|
||||
"lookup_invoice",
|
||||
"list_transactions",
|
||||
"sign_message",
|
||||
];
|
||||
const maxAmount = options.maxAmount || 100_000_000;
|
||||
const satsAmount = maxAmount / 1000;
|
||||
|
||||
const [isCreatingConnection, setCreatingConnection] = React.useState(false);
|
||||
const wallets = useAppStore((store) => store.wallets);
|
||||
const selectedWalletId = useAppStore((store) => store.selectedWalletId);
|
||||
|
||||
const [deeplinkConnectionSecret, setDeeplinkConnectionSecret] =
|
||||
React.useState<string | null>(null);
|
||||
const [redirectCountdown, setRedirectCountdown] = React.useState<number>();
|
||||
const [connectionCreated, setConnectionCreated] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (redirectCountdown === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (redirectCountdown === 0) {
|
||||
(async () => {
|
||||
if (returnTo) {
|
||||
let url = new URL(returnTo);
|
||||
if (deeplinkConnectionSecret) {
|
||||
url.searchParams.set("value", deeplinkConnectionSecret);
|
||||
}
|
||||
try {
|
||||
console.info("opening URL", url.toString());
|
||||
await openURL(url.toString());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
errorToast(
|
||||
new Error("Couldn't open URL, do you have the app installed?"),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (router.canDismiss()) {
|
||||
router.dismissAll();
|
||||
}
|
||||
router.replace("/");
|
||||
})();
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setRedirectCountdown((prev) => (prev ? prev - 1 : prev));
|
||||
}, 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [redirectCountdown, deeplinkConnectionSecret, returnTo]);
|
||||
|
||||
const confirm = async () => {
|
||||
setCreatingConnection(true);
|
||||
try {
|
||||
const nwcClient = useAppStore.getState().nwcClient;
|
||||
if (!nwcClient) {
|
||||
throw new Error("NWC client not connected");
|
||||
}
|
||||
|
||||
let secretKey: string | undefined;
|
||||
if (!pubkey) {
|
||||
const secretKeyBytes = generateSecretKey();
|
||||
secretKey = bytesToHex(secretKeyBytes);
|
||||
pubkey = getPublicKey(secretKeyBytes);
|
||||
}
|
||||
|
||||
const response = await nwcClient.createConnection({
|
||||
pubkey,
|
||||
name: name as string,
|
||||
max_amount: maxAmount,
|
||||
budget_renewal: budgetRenewal,
|
||||
request_methods: requestMethods,
|
||||
notification_types: notificationTypes,
|
||||
expires_at: expiresAt,
|
||||
isolated,
|
||||
metadata,
|
||||
});
|
||||
|
||||
console.info("createConnection response", response);
|
||||
|
||||
if (flow === "deeplink") {
|
||||
setDeeplinkConnectionSecret(
|
||||
`nostr+walletconnect://${response.wallet_pubkey}?secret=${
|
||||
secretKey
|
||||
}&relay=${nwcClient.relayUrl}`,
|
||||
);
|
||||
}
|
||||
setRedirectCountdown(3);
|
||||
setConnectionCreated(true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
errorToast(error);
|
||||
}
|
||||
setCreatingConnection(false);
|
||||
};
|
||||
|
||||
const supportsCreateConnection = useAppStore
|
||||
.getState()
|
||||
.wallets[
|
||||
useAppStore.getState().selectedWalletId
|
||||
]?.nwcCapabilities?.includes("create_connection");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Screen
|
||||
title={connectionCreated ? "App Connected" : "Connect App"}
|
||||
right={() => (
|
||||
<TouchableOpacity
|
||||
onPressIn={() => {
|
||||
if (router.canDismiss()) {
|
||||
router.dismissAll();
|
||||
}
|
||||
router.replace("/");
|
||||
}}
|
||||
className="-mr-4 px-6"
|
||||
>
|
||||
<XIcon className="text-muted-foreground" width={24} height={24} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
/>
|
||||
{!supportsCreateConnection && <NotSupportedView />}
|
||||
{supportsCreateConnection && (
|
||||
<>
|
||||
<View className="flex-1 justify-center items-center gap-8 p-6">
|
||||
<PlugView
|
||||
connectionCreated={connectionCreated}
|
||||
name={name}
|
||||
icon={icon}
|
||||
/>
|
||||
{connectionCreated && (
|
||||
<ConnectedView
|
||||
returnTo={returnTo}
|
||||
redirectCountdown={redirectCountdown}
|
||||
/>
|
||||
)}
|
||||
{!connectionCreated && (
|
||||
<ConnectView
|
||||
name={name}
|
||||
requestMethods={requestMethods}
|
||||
notificationTypes={notificationTypes}
|
||||
isolated={isolated}
|
||||
budgetRenewal={budgetRenewal}
|
||||
satsAmount={satsAmount}
|
||||
expiresAt={expiresAt}
|
||||
returnTo={returnTo}
|
||||
metadata={metadata}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
{!connectionCreated && (
|
||||
<View className="p-6">
|
||||
<View className="flex flex-row items-center justify-center gap-2 mb-4 px-4">
|
||||
<WalletIcon className="text-muted-foreground" />
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
className="text-muted-foreground font-medium2 text-xl"
|
||||
>
|
||||
{wallets[selectedWalletId].name || DEFAULT_WALLET_NAME}
|
||||
</Text>
|
||||
</View>
|
||||
<Button
|
||||
size="lg"
|
||||
onPress={confirm}
|
||||
className="flex flex-row gap-2"
|
||||
disabled={isCreatingConnection}
|
||||
>
|
||||
{isCreatingConnection && (
|
||||
<Loading className="text-primary-foreground" />
|
||||
)}
|
||||
<Text>Confirm Connection</Text>
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ConnectView({
|
||||
name,
|
||||
requestMethods,
|
||||
notificationTypes,
|
||||
isolated,
|
||||
budgetRenewal,
|
||||
satsAmount,
|
||||
expiresAt,
|
||||
returnTo,
|
||||
metadata,
|
||||
}: Omit<NWAOptions, "appPubkey" | "relayUrl"> & { satsAmount: number }) {
|
||||
const getFiatAmount = useGetFiatAmount();
|
||||
const [showDetails, setShowDetails] = React.useState(false);
|
||||
const closeModal = () => setShowDetails(false);
|
||||
const hasPayPermissions = requestMethods.some((method) =>
|
||||
method.includes("pay_"),
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
transparent
|
||||
animationType="fade"
|
||||
visible={showDetails}
|
||||
onRequestClose={closeModal}
|
||||
>
|
||||
<View className="flex-1 justify-center items-center bg-black/80">
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
onPress={closeModal}
|
||||
className="absolute inset-0"
|
||||
/>
|
||||
<View className="w-4/5 max-h-[80vh] max-w-[425px] bg-background border border-border rounded-2xl z-10">
|
||||
<View className="flex-row items-center justify-center relative p-6">
|
||||
<Text className="text-xl font-bold2 text-foreground">
|
||||
Connection Details
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={closeModal}
|
||||
className="absolute right-0 p-4"
|
||||
>
|
||||
<XIcon
|
||||
className="text-muted-foreground"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<ScrollView className="px-6">
|
||||
<View className="pb-6 flex flex-col gap-8">
|
||||
<Text className="text-xl text-center text-foreground mt-4">
|
||||
Requested methods:{" "}
|
||||
<Text className="text-xl font-semibold2 p-8">
|
||||
{requestMethods?.join(", ") || "all methods"}
|
||||
</Text>
|
||||
</Text>
|
||||
{notificationTypes && (
|
||||
<Text className="text-xl text-center text-foreground">
|
||||
Requested notification types:{" "}
|
||||
<Text className="text-xl font-semibold2 p-8">
|
||||
{notificationTypes.join(", ")}
|
||||
</Text>
|
||||
</Text>
|
||||
)}
|
||||
{isolated && (
|
||||
<Text className="text-xl text-center text-foreground font-semibold2">
|
||||
isolated connection
|
||||
</Text>
|
||||
)}
|
||||
{!!expiresAt && (
|
||||
<Text className="text-center mt-4">
|
||||
Expires {new Date(expiresAt * 1000).toDateString()}
|
||||
</Text>
|
||||
)}
|
||||
{returnTo && (
|
||||
<Text className="mt-4 text-center text-foreground">
|
||||
You will return to {returnTo} after confirming
|
||||
</Text>
|
||||
)}
|
||||
{!!metadata && (
|
||||
<Text className="mt-4 text-center text-foreground">
|
||||
Metadata: {JSON.stringify(metadata)}
|
||||
</Text>
|
||||
)}
|
||||
<Text className="mt-4 text-center text-foreground">
|
||||
You can edit permissions and revoke access at any time in your
|
||||
Alby Hub settings.
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
<Text className="text-xl text-center text-foreground mt-8">
|
||||
<Text className="text-xl font-semibold2">{name}</Text> is requesting{" "}
|
||||
{!hasPayPermissions && (
|
||||
<Text className="text-xl font-semibold2">receive-only </Text>
|
||||
)}
|
||||
access to your <Text className="text-xl font-semibold2">Alby Hub</Text>.
|
||||
</Text>
|
||||
|
||||
<View className="flex-1 flex mt-4 gap-8 justify-center">
|
||||
<View>
|
||||
{hasPayPermissions && (
|
||||
<View className="flex flex-row w-full border-2 border-muted justify-between items-center rounded-2xl p-6 py-4">
|
||||
<Text className="text-xl font-medium2">
|
||||
{budgetRenewal !== "never" && (
|
||||
<Text className="text-xl font-medium2 capitalize">
|
||||
{budgetRenewal || "Monthly"}{" "}
|
||||
</Text>
|
||||
)}
|
||||
budget
|
||||
</Text>
|
||||
<View>
|
||||
<Text className="text-right text-lg text-foreground font-medium2">
|
||||
{new Intl.NumberFormat().format(satsAmount) + " sats"}
|
||||
</Text>
|
||||
{getFiatAmount && (
|
||||
<Text className="text-right text-sm text-muted-foreground">
|
||||
{getFiatAmount(satsAmount)}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<TouchableOpacity onPress={() => setShowDetails(true)}>
|
||||
<Text className="text-center p-8">View details</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ConnectedView({
|
||||
returnTo,
|
||||
redirectCountdown,
|
||||
}: {
|
||||
returnTo?: string;
|
||||
redirectCountdown?: number;
|
||||
}) {
|
||||
return (
|
||||
<View className="flex-1 w-full justify-center items-center">
|
||||
<Tick />
|
||||
|
||||
<Text className="my-4 text-lg text-center text-foreground">
|
||||
Connected!{" "}
|
||||
{returnTo
|
||||
? `Redirecting in ${redirectCountdown} seconds...`
|
||||
: `Returning home in ${redirectCountdown} seconds...`}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function PlugView({
|
||||
connectionCreated,
|
||||
icon,
|
||||
name,
|
||||
}: {
|
||||
connectionCreated: boolean;
|
||||
icon?: string;
|
||||
name?: string;
|
||||
}) {
|
||||
const leftPlugAnim = React.useRef(new Animated.Value(0)).current;
|
||||
const rightPlugAnim = React.useRef(new Animated.Value(0)).current;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (connectionCreated) {
|
||||
Animated.parallel([
|
||||
Animated.timing(leftPlugAnim, {
|
||||
toValue: 15,
|
||||
duration: 500,
|
||||
useNativeDriver: true,
|
||||
easing: Easing.ease,
|
||||
}),
|
||||
Animated.timing(rightPlugAnim, {
|
||||
toValue: -15,
|
||||
duration: 500,
|
||||
useNativeDriver: true,
|
||||
easing: Easing.ease,
|
||||
}),
|
||||
]).start();
|
||||
} else {
|
||||
leftPlugAnim.setValue(0);
|
||||
rightPlugAnim.setValue(0);
|
||||
}
|
||||
}, [leftPlugAnim, rightPlugAnim, connectionCreated]);
|
||||
|
||||
return (
|
||||
<View className="flex flex-row items-start justify-center">
|
||||
<View className="flex items-center flex-1">
|
||||
<View className="shadow">
|
||||
<Image
|
||||
source={require("../../../assets/hub.png")}
|
||||
className="my-4 rounded-2xl w-20 h-20"
|
||||
/>
|
||||
</View>
|
||||
<Text className="text-xl font-semibold2 w-40 text-center">
|
||||
Alby Hub
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="z-[-1] relative w-36 h-28 mb-4 items-center justify-center">
|
||||
<Animated.Image
|
||||
resizeMode="contain"
|
||||
source={require("../../../assets/left-plug.png")}
|
||||
className="absolute w-28 h-28"
|
||||
style={{
|
||||
left: -45,
|
||||
transform: [
|
||||
{
|
||||
translateX: leftPlugAnim,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<Animated.Image
|
||||
resizeMode="contain"
|
||||
source={require("../../../assets/right-plug.png")}
|
||||
className="absolute w-28 h-28"
|
||||
style={{
|
||||
right: -45,
|
||||
transform: [
|
||||
{
|
||||
translateX: rightPlugAnim,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="flex items-center flex-1">
|
||||
<View className="shadow">
|
||||
{icon ? (
|
||||
<Image
|
||||
source={{ uri: icon }}
|
||||
className="my-4 rounded-2xl w-20 h-20 bg-white"
|
||||
/>
|
||||
) : (
|
||||
<View className="my-4 rounded-2xl w-20 h-20 bg-gray-200 dark:bg-gray-800 flex items-center justify-center">
|
||||
<NWCIcon width={40} height={40} opacity={0.5} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text className="text-xl font-semibold2 w-40 text-center">
|
||||
{name || "New App"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function NotSupportedView() {
|
||||
return (
|
||||
<View className="flex-1 justify-center items-center p-8">
|
||||
<View className="flex-1 justify-center items-center p-8">
|
||||
<Text className="text-center">
|
||||
This wallet connection does not support one tap connections.
|
||||
</Text>
|
||||
<Text className="text-center mt-4">
|
||||
Please re-connect Alby Go from Alby Hub using the "One Tap
|
||||
Connections" button.
|
||||
</Text>
|
||||
</View>
|
||||
{/* TODO: allow switching wallets here */}
|
||||
<Link href="/" replace className="mt-4" asChild>
|
||||
<Button variant="secondary" className="w-full">
|
||||
<Text>Home</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -6,11 +6,11 @@ import Toast from "react-native-toast-message";
|
||||
import Alert from "~/components/Alert";
|
||||
import {
|
||||
AddressIcon,
|
||||
ExportIcon,
|
||||
TrashIcon,
|
||||
TriangleAlertIcon,
|
||||
WalletIcon,
|
||||
} from "~/components/Icons";
|
||||
import NWCIcon from "~/components/icons/NWCIcon";
|
||||
import Loading from "~/components/Loading";
|
||||
import Screen from "~/components/Screen";
|
||||
import {
|
||||
@@ -121,6 +121,7 @@ export function EditWallet() {
|
||||
RNAlert.alert(
|
||||
"Export Wallet",
|
||||
"Your Wallet Connection Secret will be copied to the clipboard which you can add to another app. For per-app permission management, try out Alby Hub or add your Wallet Connection Secret to an Alby Account.",
|
||||
|
||||
[
|
||||
{
|
||||
text: "Cancel",
|
||||
@@ -129,6 +130,23 @@ export function EditWallet() {
|
||||
{
|
||||
text: "Confirm",
|
||||
onPress: () => {
|
||||
const isSuperuser = useAppStore
|
||||
.getState()
|
||||
.wallets[
|
||||
walletId
|
||||
].nwcCapabilities?.includes("create_connection");
|
||||
|
||||
if (isSuperuser) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1:
|
||||
"Connection Secret with One Tap Connections cannot be exported",
|
||||
text2:
|
||||
"Please create a new connection from Alby Hub instead",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const nwcUrl =
|
||||
useAppStore.getState().wallets[
|
||||
useAppStore.getState().selectedWalletId
|
||||
@@ -149,7 +167,7 @@ export function EditWallet() {
|
||||
>
|
||||
<Card className="w-full">
|
||||
<CardContent className="flex flex-row items-center gap-4">
|
||||
<ExportIcon className="text-muted-foreground" />
|
||||
<NWCIcon />
|
||||
<View className="flex flex-1 flex-col">
|
||||
<CardTitle>Export Wallet</CardTitle>
|
||||
<CardDescription>
|
||||
|
||||
@@ -4,11 +4,10 @@ import * as Clipboard from "expo-clipboard";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import { useAppStore } from "lib/state/appStore";
|
||||
import React from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { Modal, TouchableOpacity, View } from "react-native";
|
||||
import Toast from "react-native-toast-message";
|
||||
import Alert from "~/components/Alert";
|
||||
import DismissableKeyboardView from "~/components/DismissableKeyboardView";
|
||||
import HelpModal from "~/components/HelpModal";
|
||||
import {
|
||||
HelpCircleIcon,
|
||||
PasteIcon,
|
||||
@@ -270,3 +269,60 @@ export function SetupWallet() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function HelpModal({
|
||||
visible,
|
||||
onClose,
|
||||
}: {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Modal
|
||||
transparent
|
||||
animationType="fade"
|
||||
visible={visible}
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<View className="flex-1 justify-center items-center bg-black/80">
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
onPress={onClose}
|
||||
className="absolute inset-0"
|
||||
/>
|
||||
<View className="w-4/5 max-w-[425px] bg-background border border-border rounded-2xl z-10">
|
||||
<View className="flex-row items-center justify-center relative p-6">
|
||||
<Text className="text-xl font-bold2 text-foreground">
|
||||
Connect Your Wallet
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={onClose}
|
||||
className="absolute right-0 p-4"
|
||||
>
|
||||
<XIcon className="text-muted-foreground" width={24} height={24} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View className="p-6 pt-0 flex flex-col">
|
||||
<View className="flex flex-col mb-4">
|
||||
<Text className="text-muted-foreground">
|
||||
Follow these steps to connect Alby Go to your Hub:
|
||||
</Text>
|
||||
<Text className="text-muted-foreground">
|
||||
1. Open your Alby Hub
|
||||
</Text>
|
||||
<Text className="text-muted-foreground">
|
||||
2. Go to App Store » Alby Go
|
||||
</Text>
|
||||
<Text className="text-muted-foreground">
|
||||
3. Scan the QR code with this app
|
||||
</Text>
|
||||
</View>
|
||||
<Button onPress={onClose}>
|
||||
<Text className="font-bold2">OK</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1253,12 +1253,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@getalby/lightning-tools/-/lightning-tools-5.1.2.tgz#8a018e98d5c13097dd98d93192cf5e4e455f4c20"
|
||||
integrity sha512-BwGm8eGbPh59BVa1gI5yJMantBl/Fdps6X4p1ZACnmxz9vDINX8/3aFoOnDlF7yyA2boXWCsReVQSr26Q2yjiQ==
|
||||
|
||||
"@getalby/sdk@^3.9.0":
|
||||
version "3.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@getalby/sdk/-/sdk-3.9.0.tgz#4eb4dd9512fc41312c8a894b7f653360feb61eac"
|
||||
integrity sha512-qgNXr4FsX0a+PPvWgb112Q8h1/ov31zVP4LjsDYr5+W0CkrRbW9pQnsHPycVPLB5H8k5WVRRNkxYBBoWIBAwyw==
|
||||
"@getalby/sdk@^4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@getalby/sdk/-/sdk-4.1.0.tgz#b395cae92ffb5e814d9418e5e9f3960a996b01e2"
|
||||
integrity sha512-vo/Y6cEyFtzeOddRqS1QTmoVeathxODzMDVFHyAbfv6wZdTeqvEjVOKB2fOPatndAdZTT8nbNgFC2nhDqrvvDg==
|
||||
dependencies:
|
||||
emittery "^1.0.3"
|
||||
nostr-tools "2.9.4"
|
||||
|
||||
"@humanwhocodes/config-array@^0.13.0":
|
||||
@@ -3960,11 +3959,6 @@ emittery@^0.13.1:
|
||||
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad"
|
||||
integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==
|
||||
|
||||
emittery@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/emittery/-/emittery-1.0.3.tgz#c9d2a9c689870f15251bb13b31c67715c26d69ac"
|
||||
integrity sha512-tJdCJitoy2lrC2ldJcqN4vkqJ00lT+tOWNT1hBJjO/3FDMJa5TTIiYGCKGkn/WfCyOzUMObeohbVTj00fhiLiA==
|
||||
|
||||
emoji-regex@^10.3.0:
|
||||
version "10.4.0"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4"
|
||||
|
||||
Reference in New Issue
Block a user