Skip to content

Commit

Permalink
Add tanstack query, push notification support
Browse files Browse the repository at this point in the history
  • Loading branch information
venables committed Dec 29, 2023
1 parent d651545 commit 5c656f8
Show file tree
Hide file tree
Showing 23 changed files with 584 additions and 51 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@

- The latest [Expo 50](https://expo.dev) SDK.
- [Expo Router](https://docs.expo.dev/router/introduction/)
- [Tanstack Query](https://tanstack.com/query/v5) with persistent cache
- Toasts!
- Push Notifications
- App settings section
- [TailwindCSS](https://tailwindcss.com/) for utility-first CSS via [Nativewind v4](https://www.nativewind.dev/v4/overview).
- Built-in Dark Mode support
- CSS Variables for themes
Expand All @@ -24,10 +27,10 @@
1. Clone this repo to your desired path:

```sh
git clone git@github.com:startkit-dev/startkit-expo.git my-new-project
git clone git@github.com:startkit-dev/expo.git my-new-project
```

2. Rename the project in `app.json` and `package.json`
2. Rename the project, including `ios.bundleIdentifier` and `android.package` in `app.json` and `package.json`

3. Update your git remote to point to StartKit as `upstream`

Expand Down
3 changes: 3 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"expo": {
"name": "startkit-expo",
"slug": "startkit-expo",
"owner": "startkit",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
Expand All @@ -15,9 +16,11 @@
},
"assetBundlePatterns": ["**/*"],
"ios": {
"bundleIdentifier": "dev.startkit.expo.ios",
"supportsTablet": true
},
"android": {
"package": "dev.startkit.expo.android",
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
Expand Down
38 changes: 28 additions & 10 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import "@/assets/globals.css"
import { SpaceMono_400Regular as SpaceMono400Regular } from "@expo-google-fonts/space-mono"
import { ThemeProvider } from "@react-navigation/native"
import { useFonts } from "expo-font"
import * as Notifications from "expo-notifications"
import { SplashScreen, Stack } from "expo-router"
import { useColorScheme } from "nativewind"
import { useEffect } from "react"

import { PushNotificationsProvider } from "@/components/providers/push-notifications-provider"
import { ReactQueryProvider } from "@/components/providers/react-query-provider"
import { ToastContainer } from "@/components/toast-container"
import { useUserSettingsStore } from "@/hooks/use-user-settings"
import { DarkTheme, DefaultTheme } from "@/lib/colors/navigation-theme"
Expand All @@ -25,6 +28,17 @@ export const unstable_settings = {
// Prevent the splash screen from auto-hiding before asset loading is complete.
void SplashScreen.preventAutoHideAsync()

// This handler determines how your app handles notifications that come in while
// the app is foregrounded.
Notifications.setNotificationHandler({
handleNotification: async (_notification) =>
Promise.resolve({
shouldShowAlert: true,
shouldPlaySound: false,
shouldSetBadge: false
})
})

export default function RootLayout() {
const [isFontLoaded, error] = useFonts({
SpaceMono400Regular
Expand Down Expand Up @@ -60,17 +74,21 @@ function RootLayoutNav() {

return (
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="settings"
options={{ title: "Settings", headerBackTitle: "Back" }}
/>
<Stack.Screen name="modal" options={{ presentation: "modal" }} />
</Stack>
<ReactQueryProvider>
<PushNotificationsProvider>
<Stack initialRouteName="(tabs)">
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="settings"
options={{ headerShown: false, presentation: "modal" }}
/>
<Stack.Screen name="modal" options={{ presentation: "modal" }} />
</Stack>

{/* Toast should be the last item */}
<ToastContainer />
{/* Toast should be the last item */}
<ToastContainer />
</PushNotificationsProvider>
</ReactQueryProvider>
</ThemeProvider>
)
}
2 changes: 1 addition & 1 deletion app/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EditScreenInfo } from "@/components/edit-screen-info"
import { Text } from "@/components/text"
import { View } from "@/components/view"
import { env } from "@/env"
import { isIOS } from "@/lib/utils/platform"
import { isIOS } from "@/lib/platform"

export default function ModalScreen() {
return (
Expand Down
7 changes: 6 additions & 1 deletion app/settings/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Stack } from "expo-router"

export default function SettingsLayout() {
return <Stack screenOptions={{ headerShown: false, title: "Settings" }} />
return (
<Stack>
<Stack.Screen name="index" options={{ title: "Settings" }} />
<Stack.Screen name="debug" options={{ title: "Debug" }} />
</Stack>
)
}
98 changes: 98 additions & 0 deletions app/settings/debug.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {
CheckIcon,
ChevronRightIcon,
HeartHandshakeIcon,
VibrateIcon
} from "lucide-react-native"
import { ActivityIndicator, Linking, ScrollView } from "react-native"

import {
SettingsSection,
SettingsSectionContent,
SettingsSectionHeader,
SettingsSectionItem
} from "@/components/settings/settings"
import { Text } from "@/components/text"
import { View } from "@/components/view"
import { usePushNotificationStatus } from "@/hooks/push-notifications/use-push-notification-status"
import { useRegisterForPushNotifications } from "@/hooks/push-notifications/use-register-for-oush-notifications"
import { useTestPushNotification } from "@/hooks/push-notifications/use-test-push-notification"
import { cls } from "@/lib/cls"

export default function DebugSettingsScreen() {
const { mutate: requestPermissions, isPending } =
useRegisterForPushNotifications()

const { mutate: sendTestNotification, isPending: isSendingTestNotification } =
useTestPushNotification()

const {
isPermissionDenied,
isPermissionUndetermined,
isPermissionGranted,
isLoading: isLoadingPermissionStatus
} = usePushNotificationStatus()

return (
<View className="flex-1 bg-background">
<ScrollView contentContainerClassName="py-6 gap-8">
{/* Section: Push Notifications */}
<SettingsSection>
<SettingsSectionHeader>Push Notifications</SettingsSectionHeader>
<SettingsSectionContent>
<SettingsSectionItem
className={
isPermissionDenied || isPermissionUndetermined
? "active:opacity-50"
: undefined
}
icon={HeartHandshakeIcon}
label="Permissions"
onPress={() => {
if (isPermissionDenied) {
void Linking.openSettings()
} else if (isPermissionUndetermined) {
requestPermissions()
}
}}
>
{isLoadingPermissionStatus || isPending ? (
<ActivityIndicator size={20} />
) : isPermissionDenied ? (
<View className="flex-row gap-1">
<Text className="text-muted-foreground">Denied &ndash;</Text>
<Text className="text-primary">Fix</Text>
</View>
) : isPermissionGranted ? (
<CheckIcon className="text-primary" size={20} />
) : isPermissionUndetermined ? (
<Text className="font-bold text-primary">Request</Text>
) : (
<Text className="text-muted-foreground">Unavailable</Text>
)}
</SettingsSectionItem>

<SettingsSectionItem
className={cls(
"border-t border-border",
isPermissionGranted ? " active:opacity-50" : "opacity-50"
)}
disabled={!isPermissionGranted}
icon={VibrateIcon}
label="Send Test Notification"
onPress={() => {
sendTestNotification()
}}
>
{isSendingTestNotification ? (
<ActivityIndicator size={20} />
) : (
<ChevronRightIcon className="text-muted-foreground" size={20} />
)}
</SettingsSectionItem>
</SettingsSectionContent>
</SettingsSection>
</ScrollView>
</View>
)
}
70 changes: 36 additions & 34 deletions app/settings/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Link } from "expo-router"
import {
CheckIcon,
ChevronRightIcon,
FileDigitIcon,
Laptop2Icon,
type LucideIcon,
MoonIcon,
Expand All @@ -10,14 +13,18 @@ import { useColorScheme } from "nativewind"
import { useRef } from "react"
import { ScrollView, Switch } from "react-native"

import { Pressable } from "@/components/pressable"
import { Text } from "@/components/text"
import {
SettingsSection,
SettingsSectionContent,
SettingsSectionHeader,
SettingsSectionItem
} from "@/components/settings/settings"
import { View } from "@/components/view"
import {
type ColorScheme,
useUserSettingsStore
} from "@/hooks/use-user-settings"
import { cls } from "@/lib/utils/cls"
import { cls } from "@/lib/cls"

const THEME_SETTINGS: {
label: string
Expand Down Expand Up @@ -52,62 +59,57 @@ export default function SettingsScreen() {
<View className="flex-1 bg-background">
<ScrollView contentContainerClassName="py-6 gap-8">
{/* Section: App Theme */}
<View className="px-4">
<Text className="px-4 py-3 text-sm font-light uppercase tracking-widest text-muted-foreground">
App Theme
</Text>
<SettingsSection>
<SettingsSectionHeader>App Theme</SettingsSectionHeader>

<View className="rounded-xl border border-border">
<SettingsSectionContent>
{THEME_SETTINGS.map(({ label, icon: Icon, value }, i) => (
<Pressable
<SettingsSectionItem
className={cls(
"flex-row items-center justify-between px-4 py-3 active:opacity-50",
i !== 0 && "border-t border-border",
colorScheme === value && "bg-muted"
)}
haptics
icon={Icon}
key={value}
label={label}
onPress={() => {
onColorSchemeChange(value)
}}
>
<View className="flex-row items-center gap-3">
<Icon className="text-primary" size={18} />
<Text className="text-lg">{label}</Text>
</View>

{colorScheme === value && (
<CheckIcon className="text-primary" size={20} />
)}
</Pressable>
</SettingsSectionItem>
))}
</View>
</View>
</SettingsSectionContent>
</SettingsSection>

{/* Section: App */}
<View className="px-4">
<Text className="px-4 py-3 text-sm font-light uppercase tracking-widest text-muted-foreground">
App
</Text>

<View className="rounded-xl border border-border">
<View className="flex-row items-center justify-between px-4 py-3">
<View className="flex-row items-center gap-3">
<WavesIcon className="text-primary" size={18} />

<Text className="text-lg">Haptic Feedback</Text>
</View>
<SettingsSection>
<SettingsSectionHeader>App</SettingsSectionHeader>

<SettingsSectionContent>
<SettingsSectionItem icon={WavesIcon} label="Haptic Feedback">
<Switch
onValueChange={(val) => {
toggleHapticFeedback(val)
}}
ref={switchRef}
value={isHapticFeedbackEnabled}
/>
</View>
</View>
</View>
</SettingsSectionItem>

<Link asChild href="/settings/debug">
<SettingsSectionItem
className="active:opacity-50"
icon={FileDigitIcon}
label="Debug"
>
<ChevronRightIcon className="text-muted-foreground" size={20} />
</SettingsSectionItem>
</Link>
</SettingsSectionContent>
</SettingsSection>
</ScrollView>
</View>
)
Expand Down
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion components/external-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Link } from "expo-router"
import * as WebBrowser from "expo-web-browser"
import { type ComponentProps } from "react"

import { isWeb } from "@/lib/utils/platform"
import { isWeb } from "@/lib/platform"

export type ExternalLinkProps = Omit<ComponentProps<typeof Link>, "href"> & {
href: `${string}:${string}`
Expand Down
Loading

0 comments on commit 5c656f8

Please sign in to comment.