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:
Adithya Vardhan
2025-03-03 11:21:39 +05:30
committed by GitHub
co-authored by Roland Bewick
parent ef21b5dc65
commit 3c557652ad
17 changed files with 763 additions and 57 deletions
+10 -1
View File
@@ -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",
+5
View File
@@ -0,0 +1,5 @@
import { ConnectWallet } from "../../../../pages/settings/wallets/ConnectWallet";
export default function Page() {
return <ConnectWallet />;
}
BIN
View File
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
View File
@@ -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
View File
@@ -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 &raquo; 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 &raquo; 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>
+3
View File
@@ -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,
+30
View File
@@ -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
View File
@@ -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({
+1 -1
View File
@@ -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
View File
@@ -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",
+6
View File
@@ -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();
+506
View File
@@ -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>
);
}
+20 -2
View File
@@ -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>
+58 -2
View File
@@ -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 &raquo; 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>
);
}
+4 -10
View File
@@ -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"