Skip to content

Commit

Permalink
feat: NWA + NWC deep linking (#272)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
im-adithya and rolznz authored Mar 3, 2025
1 parent ef21b5d commit 3c55765
Show file tree
Hide file tree
Showing 17 changed files with 763 additions and 57 deletions.
11 changes: 10 additions & 1 deletion app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions app/(app)/settings/wallets/connect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ConnectWallet } from "../../../../pages/settings/wallets/ConnectWallet";

export default function Page() {
return <ConnectWallet />;
}
Binary file added assets/hub.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/left-plug.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/right-plug.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 31 additions & 23 deletions components/BTCMapModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
47 changes: 30 additions & 17 deletions components/HelpModal.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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>
Expand Down
3 changes: 3 additions & 0 deletions components/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -56,6 +57,7 @@ interopIcon(BookUserIcon);
interopIcon(CameraOffIcon);
interopIcon(CheckCircleIcon);
interopIcon(ChevronUpIcon);
interopIcon(ChevronRightIcon);
interopIcon(CopyIcon);
interopIcon(EditIcon);
interopIcon(ExportIcon);
Expand Down Expand Up @@ -91,6 +93,7 @@ export {
BookUserIcon,
CameraOffIcon,
CheckCircleIcon,
ChevronRightIcon,
ChevronUpIcon,
CopyIcon,
EditIcon,
Expand Down
30 changes: 30 additions & 0 deletions components/icons/NWCIcon.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 58 additions & 0 deletions lib/link.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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
Expand All @@ -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();
Expand Down Expand Up @@ -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({
Expand Down
2 changes: 1 addition & 1 deletion lib/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions pages/send/Send.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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();

Expand Down
Loading

0 comments on commit 3c55765

Please sign in to comment.