Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add infos about how to connect #108

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import * as Font from "expo-font";
import { useInfo } from "~/hooks/useInfo";
import { secureStorage } from "~/lib/secureStorage";
import { hasOnboardedKey } from "~/lib/state/appStore";
import { PortalHost } from '@rn-primitives/portal';

const LIGHT_THEME: Theme = {
dark: false,
Expand Down Expand Up @@ -103,6 +104,7 @@ export default function RootLayout() {
<SafeAreaView className="w-full h-full bg-background">
<Stack />
<Toast config={toastConfig} position="bottom" bottomOffset={140} />
<PortalHost />
</SafeAreaView>
</ThemeProvider>
</SWRConfig>
Expand Down
4 changes: 4 additions & 0 deletions components/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
CameraOff,
Palette,
Egg,
HelpCircle
} from "lucide-react-native";
import { cssInterop } from "nativewind";

Expand Down Expand Up @@ -83,6 +84,8 @@ interopIcon(Power);
interopIcon(CameraOff);
interopIcon(Palette);
interopIcon(Egg);
interopIcon(HelpCircle);


export {
AlertCircle,
Expand Down Expand Up @@ -119,4 +122,5 @@ export {
Power,
Palette,
Egg,
HelpCircle,
};
66 changes: 31 additions & 35 deletions components/QRCodeScanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,62 +9,58 @@ import { CameraOff } from "./Icons";

type QRCodeScannerProps = {
onScanned: (data: string) => void;
scanning?: boolean;
};

function QRCodeScanner({ onScanned }: QRCodeScannerProps) {
const [isScanning, setScanning] = React.useState(false);
function QRCodeScanner({ onScanned, scanning = true }: QRCodeScannerProps) {
const [isLoading, setLoading] = React.useState(false);
const [permissionStatus, setPermissionStatus] = React.useState(PermissionStatus.UNDETERMINED);

useEffect(() => {
// Add some timeout to allow the screen transition to finish before
// starting the camera to avoid stutters
setLoading(true);
window.setTimeout(async () => {
await scan();
setLoading(false);
}, 200);
}, []);
if (scanning) {
// Add some timeout to allow the screen transition to finish before
// starting the camera to avoid stutters
setLoading(true);
window.setTimeout(async () => {
await requestCameraPermission();
setLoading(false);
}, 200);
}
}, [scanning]);

async function scan() {
async function requestCameraPermission() {
const { status } = await Camera.requestCameraPermissionsAsync();
setPermissionStatus(status);
setScanning(status === "granted");
}

const handleScanned = (data: string) => {
setScanning((current) => {
if (current === true) {
console.log(`Bar code with data ${data} has been scanned!`);
onScanned(data);
return true;
}
return false;
});
if (scanning) {
console.log(`Bar code with data ${data} has been scanned!`);
onScanned(data);
}
};

return (
<>
{isLoading && (
{isLoading || !scanning && (
<View className="flex-1 justify-center items-center">
<Loading />
</View>
)}
{!isLoading && <>
{!isScanning && permissionStatus === PermissionStatus.DENIED &&
<View className="flex-1 h-full flex flex-col items-center justify-center gap-2 p-6">
<CameraOff className="text-foreground" size={64} />
<Text className="text-2xl text-foreground text-center">Camera Permission Denied</Text>
<Text className="text-muted-foreground text-xl text-center">It seems you denied permissions to use your camera. You might need to go to your device settings to allow access to your camera again.</Text>
</View>
}
{isScanning && (
<>
{!isLoading && scanning && (
<>
{permissionStatus === PermissionStatus.DENIED && (
<View className="flex-1 h-full flex flex-col items-center justify-center gap-2 p-6">
<CameraOff className="text-foreground" size={64} />
<Text className="text-2xl text-foreground text-center">Camera Permission Denied</Text>
<Text className="text-muted-foreground text-xl text-center">It seems you denied permissions to use your camera. You might need to go to your device settings to allow access to your camera again.</Text>
</View>
)}
{permissionStatus === PermissionStatus.GRANTED && (
<FocusableCamera onScanned={handleScanned} />
</>
)}
</>
}
)}
</>
)}
</>
);
}
Expand Down
148 changes: 148 additions & 0 deletions components/ui/dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import * as DialogPrimitive from '@rn-primitives/dialog';
import * as React from 'react';
import { Platform, StyleSheet, View } from 'react-native';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
import { cn } from '~/lib/utils';
import { X } from '../Icons';

const Dialog = DialogPrimitive.Root;

const DialogTrigger = DialogPrimitive.Trigger;

const DialogPortal = DialogPrimitive.Portal;

const DialogClose = DialogPrimitive.Close;

const DialogOverlayWeb = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => {
const { open } = DialogPrimitive.useRootContext();
return (
<DialogPrimitive.Overlay
className={cn(
'z-50 bg-black/80 flex justify-center items-center p-2 absolute top-0 right-0 bottom-0 left-0',
open ? 'web:animate-in web:fade-in-0' : 'web:animate-out web:fade-out-0',
className
)}
{...props}
ref={ref}
/>
);
});

DialogOverlayWeb.displayName = 'DialogOverlayWeb';

const DialogOverlayNative = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, children, ...props }, ref) => {
return (
<DialogPrimitive.Overlay
style={StyleSheet.absoluteFill}
className={cn('z-50 flex bg-black/80 justify-center items-center p-2', className)}
{...props}
ref={ref}
>
<Animated.View entering={FadeIn.duration(150)} exiting={FadeOut.duration(150)}>
<>{children}</>
</Animated.View>
</DialogPrimitive.Overlay>
);
});

DialogOverlayNative.displayName = 'DialogOverlayNative';

const DialogOverlay = Platform.select({
web: DialogOverlayWeb,
default: DialogOverlayNative,
});

const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { portalHost?: string }
>(({ className, children, portalHost, ...props }, ref) => {
const { open } = DialogPrimitive.useRootContext();
return (
<DialogPortal hostName={portalHost}>
<DialogOverlay>
<DialogPrimitive.Content
ref={ref}
className={cn(
'z-50 max-w-lg gap-4 border border-border web:cursor-default bg-background p-6 shadow-lg web:duration-200 rounded-lg',
open
? 'web:animate-in web:fade-in-0 web:zoom-in-95'
: 'web:animate-out web:fade-out-0 web:zoom-out-95',
className
)}
{...props}
>
{children}
<DialogPrimitive.Close
className={
'absolute right-4 top-4 p-0.5 web:group rounded-sm opacity-70 web:ring-offset-background web:transition-opacity web:hover:opacity-100 web:focus:outline-none web:focus:ring-2 web:focus:ring-ring web:focus:ring-offset-2 web:disabled:pointer-events-none'
}
>
<X
className={cn('text-muted-foreground', open && 'text-accent-foreground')}
/>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogOverlay>
</DialogPortal>
);
});
DialogContent.displayName = DialogPrimitive.Content.displayName;

const DialogHeader = ({ className, ...props }: React.ComponentPropsWithoutRef<typeof View>) => (
<View className={cn('flex flex-col gap-1.5 text-center sm:text-left', className)} {...props} />
);
DialogHeader.displayName = 'DialogHeader';

const DialogFooter = ({ className, ...props }: React.ComponentPropsWithoutRef<typeof View>) => (
<View
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end gap-2', className)}
{...props}
/>
);
DialogFooter.displayName = 'DialogFooter';

const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
'text-lg native:text-xl text-foreground font-semibold leading-none tracking-tight',
className
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;

const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm native:text-base text-muted-foreground', className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;

export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"@getalby/lightning-tools": "^5.0.3",
"@getalby/sdk": "^3.7.0",
"@react-native-async-storage/async-storage": "1.23.1",
"@rn-primitives/dialog": "^1.0.3",
"@rn-primitives/portal": "^1.0.3",
"bech32": "^2.0.0",
"buffer": "^6.0.3",
"class-variance-authority": "^0.7.0",
Expand Down
47 changes: 33 additions & 14 deletions pages/settings/wallets/WalletConnection.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Pressable, Text, View } from "react-native";
import React from "react";
import React, { useEffect } from "react";
import * as Clipboard from "expo-clipboard";
import { nwc } from "@getalby/sdk";
import { ClipboardPaste, X } from "~/components/Icons";
import { useAppStore } from "lib/state/appStore";
import { Camera } from "expo-camera/legacy"; // TODO: check if Android camera detach bug is fixed and update camera
import { router } from "expo-router";
import { Button } from "~/components/ui/button";
import { useInfo } from "~/hooks/useInfo";
Expand All @@ -15,29 +14,27 @@ import { Nip47Capability } from "@getalby/sdk/dist/NWCClient";
import Loading from "~/components/Loading";
import QRCodeScanner from "~/components/QRCodeScanner";
import Screen from "~/components/Screen";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose } from "~/components/ui/dialog";

export function WalletConnection() {
const hasConnection = useAppStore((store) => !!store.nwcClient);
const walletIdWithConnection = useAppStore((store) =>
store.wallets.findIndex((wallet) => wallet.nostrWalletConnectUrl),
);
const [isScanning, setScanning] = React.useState(false);
const [isConnecting, setConnecting] = React.useState(false);
const isFirstConnection = useAppStore((store) => !store.wallets.length)
const { data: walletInfo } = useInfo();
const { data: balance } = useBalance();

async function scan() {
const { status } = await Camera.requestCameraPermissionsAsync();
setScanning(status === "granted");
}
const [isScanning, setScanning] = React.useState(false);
const [isConnecting, setConnecting] = React.useState(false);
const [dialogOpen, setDialogOpen] = React.useState(isFirstConnection);

const handleScanned = (data: string) => {
return connect(data);
};

React.useEffect(() => {
scan();
}, []);
useEffect(() => {
setScanning(!dialogOpen);
}, [dialogOpen]);

async function paste() {
let nostrWalletConnectUrl;
Expand Down Expand Up @@ -127,7 +124,7 @@ export function WalletConnection() {
variant="destructive"
onPress={() => {
useAppStore.getState().removeNostrWalletConnectUrl();
scan();
setScanning(true);
}}
>
<Text>Disconnect Wallet</Text>
Expand All @@ -144,9 +141,31 @@ export function WalletConnection() {
</View>
</>
)}

<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="sm:max-w-[425px] ">
<DialogHeader>
<DialogTitle>Connect Your Wallet</DialogTitle>
<View className="flex flex-col gap-2">
<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>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="secondary">
<Text>OK</Text>
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>

{!isConnecting && (
<>
<QRCodeScanner onScanned={handleScanned} />
<QRCodeScanner onScanned={handleScanned} scanning={isScanning} />
<View className="flex flex-row items-stretch justify-center gap-4 p-6">
<Button
onPress={paste}
Expand Down
Loading
Loading