From e700ca8d3d676314e6a0fc5dae331da43d700d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= <100827540+reneaaron@users.noreply.github.com> Date: Mon, 2 Sep 2024 13:58:55 +0200 Subject: [PATCH] feat: onboarding (#44) * feat: onboarding * fix: allow deleting storage item * fix: upgrade packages * fix: remove button * fix: use securestorage * fix: onboarding redirect * fix: onboarding redirects * fix: button styles, copy * fix: secondary button text color --------- Co-authored-by: Roland Bewick --- app/_layout.tsx | 24 ++++++++-- app/onboarding.js | 5 +++ components/Icons.tsx | 3 ++ components/ui/button.tsx | 8 ++-- global.css | 2 +- lib/state/appStore.ts | 9 ++++ package.json | 4 +- pages/Onboarding.tsx | 39 ++++++++++++++++ pages/Wildcard.tsx | 8 +++- pages/settings/Settings.tsx | 89 +++++++++++++++++++++---------------- yarn.lock | 26 +++++------ 11 files changed, 155 insertions(+), 62 deletions(-) create mode 100644 app/onboarding.js create mode 100644 pages/Onboarding.tsx diff --git a/app/_layout.tsx b/app/_layout.tsx index 3b27ad4..0a64f57 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,9 +1,13 @@ import "../lib/applyGlobalPolyfills"; import "~/global.css"; - import { Theme, ThemeProvider } from "@react-navigation/native"; -import { SplashScreen, Stack } from "expo-router"; +import { + router, + SplashScreen, + Stack, + useRootNavigationState, +} from "expo-router"; import { StatusBar } from "expo-status-bar"; import * as React from "react"; import { SafeAreaView } from "react-native"; @@ -16,6 +20,7 @@ import Toast from "react-native-toast-message"; import { toastConfig } from "~/components/ToastConfig"; import * as Font from "expo-font"; import { useInfo } from "~/hooks/useInfo"; +import { secureStorage } from "~/lib/secureStorage"; const LIGHT_THEME: Theme = { dark: false, @@ -41,9 +46,22 @@ SplashScreen.preventAutoHideAsync(); export default function RootLayout() { const { isDarkColorScheme } = useColorScheme(); const [fontsLoaded, setFontsLoaded] = React.useState(false); + const [showOnboarding, setShowOnboarding] = React.useState(false); useConnectionChecker(); + const rootNavigationState = useRootNavigationState(); + const hasNavigationState = !!rootNavigationState?.key; + React.useEffect(() => { + const checkOnboardingStatus = async () => { + const hasOnboarded = await secureStorage.getItem("hasOnboarded"); + if (!hasOnboarded && hasNavigationState) { + router.replace("/onboarding"); + } + }; + + checkOnboardingStatus(); + (async () => { await Font.loadAsync({ OpenRunde: require("./../assets/fonts/OpenRunde-Regular.otf"), @@ -56,7 +74,7 @@ export default function RootLayout() { })().finally(() => { SplashScreen.hideAsync(); }); - }, []); + }, [hasNavigationState]); if (!fontsLoaded) { return null; diff --git a/app/onboarding.js b/app/onboarding.js new file mode 100644 index 0000000..cc0c955 --- /dev/null +++ b/app/onboarding.js @@ -0,0 +1,5 @@ +import { Onboarding } from "../pages/Onboarding"; + +export default function Page() { + return ; +} diff --git a/components/Icons.tsx b/components/Icons.tsx index 8d63b04..2fe4bf0 100644 --- a/components/Icons.tsx +++ b/components/Icons.tsx @@ -33,6 +33,7 @@ import { Power, CameraOff, Palette, + Egg, } from "lucide-react-native"; import { cssInterop } from "nativewind"; @@ -81,6 +82,7 @@ interopIcon(Hotel); interopIcon(Power); interopIcon(CameraOff); interopIcon(Palette); +interopIcon(Egg); export { AlertCircle, @@ -116,4 +118,5 @@ export { X, Power, Palette, + Egg, }; diff --git a/components/ui/button.tsx b/components/ui/button.tsx index 027216e..77804e6 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -22,7 +22,7 @@ const buttonVariants = cva( size: { default: "min-h-10 px-4 py-2 native:min-h-12 native:px-5 native:py-3", sm: "min-h-9 rounded-md px-3", - lg: "min-h-11 rounded-lg px-8 native:min-h-16", + lg: "min-h-11 rounded-2xl px-8 native:min-h-16", icon: "min-h-10 min-w-10", }, }, @@ -42,14 +42,14 @@ const buttonTextVariants = cva( destructive: "text-destructive-foreground", outline: "group-active:text-accent-foreground", secondary: - "text-secondary-foreground group-active:text-secondary-foreground font-medium2", + "text-secondary-foreground group-active:text-secondary-foreground", ghost: "group-active:text-accent-foreground", link: "text-primary group-active:underline", }, size: { - default: "", + default: "font-medium2", sm: "", - lg: "native:text-2xl", + lg: "native:text-2xl font-bold2", icon: "", }, }, diff --git a/global.css b/global.css index 61f120a..a6a1786 100644 --- a/global.css +++ b/global.css @@ -13,7 +13,7 @@ --primary: 47 100% 50%; --primary-foreground: 217 19% 27%; --secondary: 240 4.8% 95.9%; - --secondary-foreground: 215 28% 17%; + --secondary-foreground: 217 19% 27%; --muted: 240 4.8% 95.9%; --muted-foreground: 240 3.8% 46.1%; --accent: 240 4.8% 95.9%; diff --git a/lib/state/appStore.ts b/lib/state/appStore.ts index 34e3c68..b2441ed 100644 --- a/lib/state/appStore.ts +++ b/lib/state/appStore.ts @@ -19,12 +19,14 @@ interface AppState { addWallet(wallet: Wallet): void; addAddressBookEntry(entry: AddressBookEntry): void; reset(): void; + showOnboarding(): void; } const walletKeyPrefix = "wallet"; const addressBookEntryKeyPrefix = "addressBookEntry"; const selectedWalletIdKey = "selectedWalletId"; const fiatCurrencyKey = "fiatCurrency"; +const hasOnboardedKey = "hasOnboarded"; type Wallet = { name?: string; @@ -176,6 +178,10 @@ export const useAppStore = create()((set, get) => { addressBookEntries: [...currentAddressBookEntries, addressBookEntry], }); }, + showOnboarding() { + // clear onboarding status + secureStorage.removeItem(hasOnboardedKey); + }, reset() { // clear wallets for (let i = 0; i < get().wallets.length; i++) { @@ -188,6 +194,9 @@ export const useAppStore = create()((set, get) => { // clear selected wallet ID secureStorage.removeItem(selectedWalletIdKey); + // clear onboarding status + secureStorage.removeItem(hasOnboardedKey); + set({ nwcClient: undefined, fiatCurrency: undefined, diff --git a/package.json b/package.json index a8aa888..2968037 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "dayjs": "^1.11.10", - "expo": "^51.0.30", - "expo-camera": "^15.0.14", + "expo": "~51.0.31", + "expo-camera": "~15.0.15", "expo-clipboard": "~6.0.3", "expo-constants": "~16.0.2", "expo-font": "^12.0.9", diff --git a/pages/Onboarding.tsx b/pages/Onboarding.tsx new file mode 100644 index 0000000..35e36b1 --- /dev/null +++ b/pages/Onboarding.tsx @@ -0,0 +1,39 @@ +import { openURL } from "expo-linking"; +import { Link, Stack, router } from "expo-router"; +import React from "react"; +import { View } from "react-native"; +import { Button } from "~/components/ui/button"; +import { Text } from "~/components/ui/text"; +import { secureStorage } from "~/lib/secureStorage"; + +export function Onboarding() { + async function finish() { + secureStorage.setItem("hasOnboarded", "true"); + router.replace("/"); + } + + return ( + + + + Hello there 👋 + + Alby Go is a simple mobile wallet interface for your Alby Hub or other lightning nodes and wallets. + + + + + + + + ); +} diff --git a/pages/Wildcard.tsx b/pages/Wildcard.tsx index 266a3d2..3cf5a18 100644 --- a/pages/Wildcard.tsx +++ b/pages/Wildcard.tsx @@ -1,4 +1,4 @@ -import { usePathname } from "expo-router"; +import { Stack, usePathname } from "expo-router"; import { View } from "react-native"; import Loading from "~/components/Loading"; import { Text } from "~/components/ui/text"; @@ -10,6 +10,12 @@ export function Wildcard() { return ( + null, // hide header completely + }} + /> Loading {pathname} diff --git a/pages/settings/Settings.tsx b/pages/settings/Settings.tsx index 1e9ccfe..1f2b584 100644 --- a/pages/settings/Settings.tsx +++ b/pages/settings/Settings.tsx @@ -1,6 +1,6 @@ import { Link, router } from "expo-router"; import { Alert, Pressable, TouchableOpacity, View } from "react-native"; -import { Bitcoin, Palette, Power, Wallet2 } from "~/components/Icons"; +import { Bitcoin, Egg, Palette, Power, Wallet2 } from "~/components/Icons"; import { DEFAULT_WALLET_NAME } from "~/lib/constants"; import { useAppStore } from "~/lib/state/appStore"; @@ -25,9 +25,7 @@ export function Settings() { - - Wallets - + Wallets ({wallet.name || DEFAULT_WALLET_NAME}) @@ -43,47 +41,63 @@ export function Settings() { - + - - Theme - + Theme ({colorScheme.charAt(0).toUpperCase() + colorScheme.substring(1)}) {developerMode && ( - - Developer mode - { - Alert.alert( - "Reset", - "Are you sure you want to reset? You will be signed out of all your wallets. Your connection secrets and address book will be lost.", - [ - { - text: "Cancel", - style: "cancel", - }, - { - text: "Confirm", - onPress: () => { - router.dismissAll(); - useAppStore.getState().reset(); - }, - }, - ], - ); - }} - > - - - Reset Wallet + <> + + + Developer mode - - + { + router.dismissAll(); + useAppStore.getState().showOnboarding(); + router.replace("/onboarding"); + }} + > + + Open Onboarding + + { + Alert.alert( + "Reset", + "Are you sure you want to reset? You will be signed out of all your wallets. Your connection secrets and address book will be lost.", + [ + { + text: "Cancel", + style: "cancel", + }, + { + text: "Confirm", + onPress: () => { + router.dismissAll(); + useAppStore.getState().reset(); + }, + }, + ], + ); + }} + > + + + Reset Wallet + + + + )} 1 && newCounter < 5) { Toast.show({ diff --git a/yarn.lock b/yarn.lock index 80b4f37..631d071 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3604,10 +3604,10 @@ expo-asset@~10.0.10: invariant "^2.2.4" md5-file "^3.2.3" -expo-camera@^15.0.14: - version "15.0.14" - resolved "https://registry.yarnpkg.com/expo-camera/-/expo-camera-15.0.14.tgz#1fec9c0e650cfa5b85d34ce8267ae8a9854eee4a" - integrity sha512-O4uvVywGsQ3a89d0BX4lq6mDuGYGukx1PYY4QrC9zw1yzD2W9BVTl8lanFRPC5h4PRniekfeWUVH1a0jJmkLIw== +expo-camera@~15.0.15: + version "15.0.15" + resolved "https://registry.yarnpkg.com/expo-camera/-/expo-camera-15.0.15.tgz#2959963b65219ca5d101327b3298b4170a4e107b" + integrity sha512-zJS0rfOwGfyDrccMsFaLH9s0mW0f6czw+W+RHvbyAdAmoAh6ZtzZRGbosKIYoRsn/8L7ajNp01seJUjsT0FtzA== dependencies: invariant "^2.2.4" @@ -3674,10 +3674,10 @@ expo-modules-autolinking@1.11.2: require-from-string "^2.0.2" resolve-from "^5.0.0" -expo-modules-core@1.12.22: - version "1.12.22" - resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-1.12.22.tgz#2251577af969edaa0d7808b37b9f9b8493afefe5" - integrity sha512-MX9qJRuVszyuGksOZ1QkMcUGcAZ1o2AmDigkQAl9yxqFtwEpBOxELs3rXeQol7WiEabvK+bERTF9LtSTDCVCYw== +expo-modules-core@1.12.23: + version "1.12.23" + resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-1.12.23.tgz#ea1c0c876aa52db2cb9d25cb59404c334f92446f" + integrity sha512-NYp/rWhKW6zlqNdC8/r+FckzlAGWX0IJEjOxwYHuYeRUn/vnKksb43G4E3jcaQEZgmWlKxK4LpxL3gr7m0RJFA== dependencies: invariant "^2.2.4" @@ -3713,10 +3713,10 @@ expo-status-bar@~1.12.1: resolved "https://registry.yarnpkg.com/expo-status-bar/-/expo-status-bar-1.12.1.tgz#52ce594aab5064a0511d14375364d718ab78aa66" integrity sha512-/t3xdbS8KB0prj5KG5w7z+wZPFlPtkgs95BsmrP/E7Q0xHXTcDcQ6Cu2FkFuRM+PKTb17cJDnLkawyS5vDLxMA== -expo@^51.0.30: - version "51.0.30" - resolved "https://registry.yarnpkg.com/expo/-/expo-51.0.30.tgz#5827ecef5e273238258cc808cefb473eaf5fce69" - integrity sha512-eo4T2TGnKyDkLryY6lYuGVV1FZyWa1FG7Ns0aqfZ7N/PFQYDfbowVwkcEWbRppSzqSfjGXYqvVV4WZNFbpgZZw== +expo@~51.0.31: + version "51.0.31" + resolved "https://registry.yarnpkg.com/expo/-/expo-51.0.31.tgz#edd623e718705d88681406e72869076dfeb485ff" + integrity sha512-YiUNcxzSkQ0jlKW+e8F81KnZfAhCugEZI9VYmuIsFONHivtiYIADHdcFvUWnexUEdgPQDkgWw85XBnIbzIZ39Q== dependencies: "@babel/runtime" "^7.20.0" "@expo/cli" "0.18.29" @@ -3730,7 +3730,7 @@ expo@^51.0.30: expo-font "~12.0.9" expo-keep-awake "~13.0.2" expo-modules-autolinking "1.11.2" - expo-modules-core "1.12.22" + expo-modules-core "1.12.23" fbemitter "^3.0.0" whatwg-url-without-unicode "8.0.0-3"