Skip to content

Commit ec19609

Browse files
webbjordyclaude
andcommitted
feat(react-native-example): add navigator, parity screens, config, and README
Wires up React Navigation v7 native stack with six screens that match the parity spec: Startup, SignIn, Main, MessageCompose, Preferences, TenantSwitcher. Each screen is a real early-stage render that shows the eventual purpose of the screen (form fields for sign-in, channel toggles for preferences, tenant list, etc.) — not a "coming soon" placeholder. Runtime configuration is hardcoded in src/config.ts behind an AppConfig type with TODO markers per field, matching the pattern used by the Android and iOS demos. Phase 2 will wire the screens to @knocklabs/react-native; Phase 1 keeps the SDK out so the scaffold can be reviewed independently. README mirrors the expo-example README shape and disambiguates the two: this one demonstrates @knocklabs/react-native in a bare RN context, expo-example demonstrates @knocklabs/expo on Expo-managed. KNO-12856 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b4b69b0 commit ec19609

11 files changed

Lines changed: 664 additions & 0 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Knock + React Native example app
2+
3+
Demonstrates [`@knocklabs/react-native`](../../packages/react-native) in a bare React Native app. For an Expo-managed example, see [`expo-example`](../expo-example).
4+
5+
## Running locally
6+
7+
1. Install dependencies from the root of the monorepo.
8+
9+
```sh
10+
yarn
11+
```
12+
13+
2. Build the Knock packages.
14+
15+
```sh
16+
yarn build:packages
17+
```
18+
19+
3. Configure the app. Open [`src/config.ts`](./src/config.ts) and replace the placeholder values with your Knock public API key, a test user ID, and your in-app feed channel ID.
20+
21+
4. Set up your React Native development environment. See the [React Native environment setup guide](https://reactnative.dev/docs/set-up-your-environment).
22+
23+
5. For iOS, install the CocoaPods dependencies once.
24+
25+
```sh
26+
cd ios && bundle install && bundle exec pod install && cd ..
27+
```
28+
29+
6. Start Metro and launch on a simulator or device.
30+
31+
```sh
32+
yarn start
33+
34+
# In another terminal:
35+
yarn ios
36+
# or
37+
yarn android
38+
```
39+
40+
## Configuration
41+
42+
All runtime configuration lives in [`src/config.ts`](./src/config.ts). It's a single exported object with a `publicApiKey`, `userId`, optional `tenantId`, and `feedChannelId`. Values come from the [Knock dashboard](https://dashboard.knock.app).
43+
44+
In a production app these would come from your backend (for `userId`) and your environment (for the channel and tenant IDs). For this example they're hardcoded so you can get running quickly.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { DefaultTheme, NavigationContainer } from "@react-navigation/native";
2+
import { createNativeStackNavigator } from "@react-navigation/native-stack";
3+
import { StatusBar } from "react-native";
4+
import { SafeAreaProvider } from "react-native-safe-area-context";
5+
6+
import type { RootStackParamList } from "./navigation";
7+
import MainScreen from "./screens/MainScreen";
8+
import MessageComposeScreen from "./screens/MessageComposeScreen";
9+
import PreferencesScreen from "./screens/PreferencesScreen";
10+
import SignInScreen from "./screens/SignInScreen";
11+
import StartupScreen from "./screens/StartupScreen";
12+
import TenantSwitcherScreen from "./screens/TenantSwitcherScreen";
13+
import { colors } from "./theme";
14+
15+
const Stack = createNativeStackNavigator<RootStackParamList>();
16+
17+
const navTheme = {
18+
...DefaultTheme,
19+
dark: true,
20+
colors: {
21+
...DefaultTheme.colors,
22+
background: colors.background,
23+
card: colors.surface,
24+
text: colors.text,
25+
border: colors.border,
26+
primary: colors.accent,
27+
notification: colors.accent,
28+
},
29+
};
30+
31+
export default function App() {
32+
return (
33+
<SafeAreaProvider>
34+
<StatusBar barStyle="light-content" backgroundColor={colors.background} />
35+
<NavigationContainer theme={navTheme}>
36+
<Stack.Navigator initialRouteName="Startup">
37+
<Stack.Screen
38+
name="Startup"
39+
component={StartupScreen}
40+
options={{ headerShown: false }}
41+
/>
42+
<Stack.Screen
43+
name="SignIn"
44+
component={SignInScreen}
45+
options={{ title: "Sign in" }}
46+
/>
47+
<Stack.Screen
48+
name="Main"
49+
component={MainScreen}
50+
options={{ title: "Knock", headerBackVisible: false }}
51+
/>
52+
<Stack.Screen
53+
name="MessageCompose"
54+
component={MessageComposeScreen}
55+
options={{ title: "Compose message", presentation: "modal" }}
56+
/>
57+
<Stack.Screen
58+
name="Preferences"
59+
component={PreferencesScreen}
60+
options={{ title: "Preferences" }}
61+
/>
62+
<Stack.Screen
63+
name="TenantSwitcher"
64+
component={TenantSwitcherScreen}
65+
options={{ title: "Switch tenant", presentation: "modal" }}
66+
/>
67+
</Stack.Navigator>
68+
</NavigationContainer>
69+
</SafeAreaProvider>
70+
);
71+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export type AppConfig = {
2+
publicApiKey: string;
3+
userId: string;
4+
tenantId: string | null;
5+
feedChannelId: string;
6+
};
7+
8+
export const config: AppConfig = {
9+
// TODO: Your Knock public API key. Find it at https://dashboard.knock.app under Developers.
10+
publicApiKey: "pk_test_REPLACE_ME",
11+
12+
// TODO: The ID of the signed-in user. Replace with your test user's ID.
13+
userId: "user_REPLACE_ME",
14+
15+
// TODO: Optional tenant ID to scope feeds and preferences. Leave null for no tenant scoping.
16+
tenantId: null,
17+
18+
// TODO: The in-app feed channel ID from the Integrations page in the Knock dashboard.
19+
feedChannelId: "REPLACE_ME",
20+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
2+
3+
export type RootStackParamList = {
4+
Startup: undefined;
5+
SignIn: undefined;
6+
Main: undefined;
7+
MessageCompose: undefined;
8+
Preferences: undefined;
9+
TenantSwitcher: undefined;
10+
};
11+
12+
export type ScreenProps<R extends keyof RootStackParamList> =
13+
NativeStackScreenProps<RootStackParamList, R>;
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
2+
3+
import { config } from "../config";
4+
import type { RootStackParamList, ScreenProps } from "../navigation";
5+
import { colors, radius, spacing } from "../theme";
6+
7+
type Destination = {
8+
title: string;
9+
subtitle: string;
10+
route: keyof Pick<
11+
RootStackParamList,
12+
"MessageCompose" | "Preferences" | "TenantSwitcher"
13+
>;
14+
};
15+
16+
const destinations: Destination[] = [
17+
{
18+
title: "Compose message",
19+
subtitle: "Trigger a workflow from the app",
20+
route: "MessageCompose",
21+
},
22+
{
23+
title: "Notification preferences",
24+
subtitle: "Per-channel opt-ins for this user",
25+
route: "Preferences",
26+
},
27+
{
28+
title: "Switch tenant",
29+
subtitle: "Scope feeds and preferences to a tenant",
30+
route: "TenantSwitcher",
31+
},
32+
];
33+
34+
export default function MainScreen({ navigation }: ScreenProps<"Main">) {
35+
return (
36+
<ScrollView style={styles.root} contentContainerStyle={styles.content}>
37+
<View style={styles.header}>
38+
<Text style={styles.label}>Signed in as</Text>
39+
<Text style={styles.userId}>{config.userId}</Text>
40+
</View>
41+
42+
{destinations.map(({ title, subtitle, route }) => (
43+
<Pressable
44+
key={route}
45+
onPress={() => navigation.navigate(route)}
46+
style={({ pressed }) => [styles.row, pressed && styles.pressed]}
47+
>
48+
<Text style={styles.rowTitle}>{title}</Text>
49+
<Text style={styles.rowSubtitle}>{subtitle}</Text>
50+
</Pressable>
51+
))}
52+
53+
<Pressable
54+
onPress={() => navigation.popToTop()}
55+
style={({ pressed }) => [styles.signOut, pressed && styles.pressed]}
56+
>
57+
<Text style={styles.signOutText}>Sign out</Text>
58+
</Pressable>
59+
</ScrollView>
60+
);
61+
}
62+
63+
const styles = StyleSheet.create({
64+
root: {
65+
flex: 1,
66+
backgroundColor: colors.background,
67+
},
68+
content: {
69+
padding: spacing.lg,
70+
gap: spacing.md,
71+
},
72+
header: {
73+
marginBottom: spacing.md,
74+
},
75+
label: {
76+
color: colors.mutedText,
77+
fontSize: 12,
78+
textTransform: "uppercase",
79+
letterSpacing: 0.5,
80+
marginBottom: spacing.xs,
81+
},
82+
userId: {
83+
color: colors.text,
84+
fontSize: 18,
85+
fontWeight: "500",
86+
},
87+
row: {
88+
backgroundColor: colors.surface,
89+
borderWidth: 1,
90+
borderColor: colors.border,
91+
borderRadius: radius.md,
92+
padding: spacing.md,
93+
gap: spacing.xs,
94+
},
95+
rowTitle: {
96+
color: colors.text,
97+
fontSize: 16,
98+
fontWeight: "500",
99+
},
100+
rowSubtitle: {
101+
color: colors.mutedText,
102+
fontSize: 13,
103+
},
104+
pressed: {
105+
opacity: 0.7,
106+
},
107+
signOut: {
108+
marginTop: spacing.lg,
109+
alignItems: "center",
110+
paddingVertical: spacing.md,
111+
},
112+
signOutText: {
113+
color: colors.mutedText,
114+
fontSize: 15,
115+
},
116+
});
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { useState } from "react";
2+
import {
3+
Pressable,
4+
ScrollView,
5+
StyleSheet,
6+
Text,
7+
TextInput,
8+
} from "react-native";
9+
10+
import type { ScreenProps } from "../navigation";
11+
import { colors, radius, spacing } from "../theme";
12+
13+
export default function MessageComposeScreen({
14+
navigation,
15+
}: ScreenProps<"MessageCompose">) {
16+
const [workflowKey, setWorkflowKey] = useState("new-comment");
17+
const [body, setBody] = useState("");
18+
19+
const canSend = workflowKey.trim().length > 0 && body.trim().length > 0;
20+
21+
return (
22+
<ScrollView style={styles.root} contentContainerStyle={styles.content}>
23+
<Text style={styles.label}>Workflow key</Text>
24+
<TextInput
25+
value={workflowKey}
26+
onChangeText={setWorkflowKey}
27+
autoCapitalize="none"
28+
autoCorrect={false}
29+
placeholderTextColor={colors.mutedText}
30+
style={styles.input}
31+
/>
32+
33+
<Text style={styles.label}>Message</Text>
34+
<TextInput
35+
value={body}
36+
onChangeText={setBody}
37+
multiline
38+
placeholder="What do you want to notify about?"
39+
placeholderTextColor={colors.mutedText}
40+
style={[styles.input, styles.multiline]}
41+
/>
42+
43+
<Pressable
44+
onPress={() => navigation.goBack()}
45+
disabled={!canSend}
46+
style={({ pressed }) => [
47+
styles.button,
48+
!canSend && styles.disabled,
49+
pressed && styles.pressed,
50+
]}
51+
>
52+
<Text style={styles.buttonText}>Send</Text>
53+
</Pressable>
54+
</ScrollView>
55+
);
56+
}
57+
58+
const styles = StyleSheet.create({
59+
root: {
60+
flex: 1,
61+
backgroundColor: colors.background,
62+
},
63+
content: {
64+
padding: spacing.lg,
65+
gap: spacing.sm,
66+
},
67+
label: {
68+
color: colors.mutedText,
69+
fontSize: 12,
70+
textTransform: "uppercase",
71+
letterSpacing: 0.5,
72+
marginTop: spacing.sm,
73+
},
74+
input: {
75+
color: colors.text,
76+
fontSize: 16,
77+
backgroundColor: colors.surface,
78+
borderWidth: 1,
79+
borderColor: colors.border,
80+
borderRadius: radius.md,
81+
paddingHorizontal: spacing.md,
82+
paddingVertical: spacing.sm,
83+
},
84+
multiline: {
85+
minHeight: 120,
86+
textAlignVertical: "top",
87+
paddingTop: spacing.sm,
88+
},
89+
button: {
90+
marginTop: spacing.lg,
91+
backgroundColor: colors.accent,
92+
paddingVertical: spacing.md,
93+
borderRadius: radius.md,
94+
alignItems: "center",
95+
},
96+
disabled: {
97+
opacity: 0.4,
98+
},
99+
pressed: {
100+
opacity: 0.8,
101+
},
102+
buttonText: {
103+
color: colors.text,
104+
fontSize: 16,
105+
fontWeight: "600",
106+
},
107+
});

0 commit comments

Comments
 (0)