Skip to content

Commit

Permalink
Merge pull request #223 from getAlby/task-nwc-linking
Browse files Browse the repository at this point in the history
feat: support nwc connection urls
  • Loading branch information
im-adithya authored Dec 20, 2024
2 parents c282510 + 427d412 commit c8a4576
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 111 deletions.
2 changes: 1 addition & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "Alby Go",
"slug": "alby-mobile",
"version": "1.7.2",
"scheme": ["lightning", "bitcoin", "alby"],
"scheme": ["lightning", "bitcoin", "alby", "nostr+walletconnect"],
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
Expand Down
20 changes: 19 additions & 1 deletion lib/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { router } from "expo-router";
import { BOLT11_REGEX } from "./constants";
import { lnurl as lnurlLib } from "./lnurl";

const SUPPORTED_SCHEMES = ["lightning:", "bitcoin:", "alby:"];
const SUPPORTED_SCHEMES = [
"lightning:",
"bitcoin:",
"alby:",
"nostr+walletconnect:",
];

// Register exp scheme for testing during development
// https://docs.expo.dev/guides/linking/#creating-urls
Expand All @@ -22,6 +27,19 @@ export const handleLink = async (url: string) => {

if (SUPPORTED_SCHEMES.indexOf(parsedUrl.protocol) > -1) {
let { username, hostname, protocol, pathname, search } = parsedUrl;
if (parsedUrl.protocol === "nostr+walletconnect:") {
if (router.canDismiss()) {
router.dismissAll();
}
console.info("Navigating to wallet setup");
router.push({
pathname: "/settings/wallets/setup",
params: {
nwcUrl: protocol + hostname + search,
},
});
return;
}

if (parsedUrl.protocol === "exp:") {
if (!parsedUrl.pathname) {
Expand Down
133 changes: 64 additions & 69 deletions pages/settings/Wallets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,76 +14,71 @@ export function Wallets() {
const selectedWalletId = useAppStore((store) => store.selectedWalletId);
const wallets = useAppStore((store) => store.wallets);
return (
<>
<View className="flex-1 flex flex-col">
<Screen title="Manage Wallets" />
<View className="flex-1 px-6 py-3">
<FlatList
className="flex flex-col"
data={wallets}
renderItem={(item) => {
const active = item.index === selectedWalletId;
<View className="flex-1 flex flex-col">
<Screen title="Manage Wallets" />
<View className="flex-1 px-6 py-3">
<FlatList
className="flex flex-col"
data={wallets}
renderItem={(item) => {
const active = item.index === selectedWalletId;

return (
<TouchableOpacity
onPress={() => {
if (item.index !== selectedWalletId) {
useAppStore.getState().setSelectedWalletId(item.index);
router.dismissAll();
router.navigate("/");
Toast.show({
type: "success",
text1: `Switched wallet to ${item.item.name || DEFAULT_WALLET_NAME}`,
position: "top",
});
}
}}
className={cn(
"flex flex-row items-center justify-between p-6 rounded-2xl border-2",
active ? "border-primary" : "border-transparent",
)}
>
<View className="flex flex-row gap-4 items-center flex-shrink">
<Wallet2 className="text-foreground" />
<Text
className={cn(
"text-xl pr-16",
active && "font-semibold2",
)}
numberOfLines={1}
ellipsizeMode="tail"
>
{item.item.name || DEFAULT_WALLET_NAME}
</Text>
</View>
{active && (
<Link
href={`/settings/wallets/${selectedWalletId}`}
className="absolute right-4"
asChild
>
<TouchableOpacity>
<Settings2 className="text-foreground w-32 h-32" />
</TouchableOpacity>
</Link>
)}
</TouchableOpacity>
);
}}
/>
</View>
<View className="p-6">
<Button
size="lg"
onPress={() => {
router.dismissAll();
router.push("/settings/wallets/setup");
}}
>
<Text>Connect a Wallet</Text>
</Button>
</View>
return (
<TouchableOpacity
onPress={() => {
if (item.index !== selectedWalletId) {
useAppStore.getState().setSelectedWalletId(item.index);
router.dismissAll();
router.navigate("/");
Toast.show({
type: "success",
text1: `Switched wallet to ${item.item.name || DEFAULT_WALLET_NAME}`,
position: "top",
});
}
}}
className={cn(
"flex flex-row items-center justify-between p-6 rounded-2xl border-2",
active ? "border-primary" : "border-transparent",
)}
>
<View className="flex flex-row gap-4 items-center flex-shrink">
<Wallet2 className="text-foreground" />
<Text
className={cn("text-xl pr-16", active && "font-semibold2")}
numberOfLines={1}
ellipsizeMode="tail"
>
{item.item.name || DEFAULT_WALLET_NAME}
</Text>
</View>
{active && (
<Link
href={`/settings/wallets/${selectedWalletId}`}
className="absolute right-4"
asChild
>
<TouchableOpacity>
<Settings2 className="text-foreground w-32 h-32" />
</TouchableOpacity>
</Link>
)}
</TouchableOpacity>
);
}}
/>
</View>
</>
<View className="p-6">
<Button
size="lg"
onPress={() => {
router.dismissAll();
router.push("/settings/wallets/setup");
}}
>
<Text>Connect a Wallet</Text>
</Button>
</View>
</View>
);
}
111 changes: 71 additions & 40 deletions pages/settings/wallets/SetupWallet.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { nwc } from "@getalby/sdk";
import { Nip47Capability } from "@getalby/sdk/dist/NWCClient";
import * as Clipboard from "expo-clipboard";
import { router } from "expo-router";
import { router, useLocalSearchParams } from "expo-router";
import { useAppStore } from "lib/state/appStore";
import React from "react";
import { Pressable, TouchableOpacity, View } from "react-native";
Expand All @@ -28,6 +28,9 @@ import { REQUIRED_CAPABILITIES } from "~/lib/constants";
import { errorToast } from "~/lib/errorToast";

export function SetupWallet() {
const { nwcUrl: nwcUrlFromSchemeLink } = useLocalSearchParams<{
nwcUrl: string;
}>();
const wallets = useAppStore((store) => store.wallets);
const walletIdWithConnection = wallets.findIndex(
(wallet) => wallet.nostrWalletConnectUrl,
Expand All @@ -39,6 +42,7 @@ export function SetupWallet() {
const [capabilities, setCapabilities] =
React.useState<nwc.Nip47Capability[]>();
const [name, setName] = React.useState("");
const [startScanning, setStartScanning] = React.useState(false);

const handleScanned = (data: string) => {
return connect(data);
Expand All @@ -56,47 +60,55 @@ export function SetupWallet() {
connect(nostrWalletConnectUrl);
}

async function connect(nostrWalletConnectUrl: string) {
try {
setConnecting(true);
// make sure connection is valid
const nwcClient = new nwc.NWCClient({
nostrWalletConnectUrl,
});
const info = await nwcClient.getInfo();
const capabilities = [...info.methods] as Nip47Capability[];
if (info.notifications?.length) {
capabilities.push("notifications");
}
if (
!REQUIRED_CAPABILITIES.every((capability) =>
capabilities.includes(capability),
)
) {
const missing = REQUIRED_CAPABILITIES.filter(
(capability) => !capabilities.includes(capability),
);
throw new Error(`Missing required capabilities: ${missing.join(", ")}`);
}
const connect = React.useCallback(
async (nostrWalletConnectUrl: string): Promise<boolean> => {
try {
setConnecting(true);
// make sure connection is valid
const nwcClient = new nwc.NWCClient({
nostrWalletConnectUrl,
});
const info = await nwcClient.getInfo();
const capabilities = [...info.methods] as Nip47Capability[];
if (info.notifications?.length) {
capabilities.push("notifications");
}
if (
!REQUIRED_CAPABILITIES.every((capability) =>
capabilities.includes(capability),
)
) {
const missing = REQUIRED_CAPABILITIES.filter(
(capability) => !capabilities.includes(capability),
);
throw new Error(
`Missing required capabilities: ${missing.join(", ")}`,
);
}

console.info("NWC connected", info);
console.info("NWC connected", info);

setNostrWalletConnectUrl(nostrWalletConnectUrl);
setCapabilities(capabilities);
setName(nwcClient.lud16 || "");
setNostrWalletConnectUrl(nostrWalletConnectUrl);
setCapabilities(capabilities);
setName(nwcClient.lud16 || "");

Toast.show({
type: "success",
text1: "Connection successful",
text2: "Please set your wallet name to finish",
position: "top",
});
} catch (error) {
console.error(error);
errorToast(error);
}
setConnecting(false);
}
Toast.show({
type: "success",
text1: "Connection successful",
text2: "Please set your wallet name to finish",
position: "top",
});
setConnecting(false);
return true;
} catch (error) {
console.error(error);
errorToast(error);
}
setConnecting(false);
return false;
},
[],
);

const addWallet = () => {
if (!nostrWalletConnectUrl) {
Expand All @@ -122,6 +134,22 @@ export function SetupWallet() {
router.replace("/");
};

React.useEffect(() => {
if (nwcUrlFromSchemeLink) {
(async () => {
const result = await connect(nwcUrlFromSchemeLink);
// Delay the camera to show the error message
if (!result) {
setTimeout(() => {
setStartScanning(true);
}, 2000);
}
})();
} else {
setStartScanning(true);
}
}, [connect, nwcUrlFromSchemeLink]);

return (
<>
<Screen
Expand Down Expand Up @@ -182,7 +210,10 @@ export function SetupWallet() {
</View>
) : !nostrWalletConnectUrl ? (
<>
<QRCodeScanner onScanned={handleScanned} startScanning />
<QRCodeScanner
onScanned={handleScanned}
startScanning={startScanning}
/>
<View className="flex flex-row items-stretch justify-center gap-4 p-6">
<Button
onPress={paste}
Expand Down

0 comments on commit c8a4576

Please sign in to comment.