Skip to content

Commit 40857ae

Browse files
webbjordyclaude
andcommitted
feat(react-native-example): wire Knock SDK and align screens with Android/iOS demos
Adds @knocklabs/react-native as a workspace dependency along with the required peer (react-native-gesture-handler). Wraps the authed stack in KnockProvider + KnockPushNotificationProvider + KnockFeedProvider, gated by a small AuthContext that lifts user identity and tenant scope to the app root. Pre-auth screens (Startup, SignIn) live in a separate stack so the navigator structure makes the auth boundary obvious. Realigns each screen against the Android (knocklabs/knock-android/knock-example-app) and iOS (knocklabs/ios-example-app) demos for parity: - Config exports six flat KNOCK_* constants matching Android's Utils.kt shape, plus KNOCK_TENANT_A and KNOCK_TENANT_B for the iOS-style tenant switcher. - Main embeds the prebuilt <NotificationFeed /> component so the in-app feed renders in real time. - Compose shows the workflow trigger form and clears the field on send. The server-side trigger pattern is documented inline; clients can't trigger workflows directly because that requires the secret API key. - Preferences uses usePreferences for live read/write of channel-type preferences and hosts the sign-out action. - TenantSwitcher offers None / Tenant A / Tenant B; selecting one updates auth state and re-mounts KnockFeedProvider with the new tenant scope. Knock packages must be built once (`yarn build:packages`) before type-checking or bundling, since the SDK exposes types from dist/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ec19609 commit 40857ae

14 files changed

Lines changed: 526 additions & 190 deletions
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
GEM
2+
remote: https://rubygems.org/
3+
specs:
4+
CFPropertyList (3.0.9)
5+
activesupport (6.1.7.10)
6+
concurrent-ruby (~> 1.0, >= 1.0.2)
7+
i18n (>= 1.6, < 2)
8+
minitest (>= 5.1)
9+
tzinfo (~> 2.0)
10+
zeitwerk (~> 2.3)
11+
addressable (2.9.0)
12+
public_suffix (>= 2.0.2, < 8.0)
13+
algoliasearch (1.27.5)
14+
httpclient (~> 2.8, >= 2.8.3)
15+
json (>= 1.5.1)
16+
atomos (0.1.3)
17+
benchmark (0.5.0)
18+
bigdecimal (4.1.2)
19+
claide (1.1.0)
20+
cocoapods (1.15.2)
21+
addressable (~> 2.8)
22+
claide (>= 1.0.2, < 2.0)
23+
cocoapods-core (= 1.15.2)
24+
cocoapods-deintegrate (>= 1.0.3, < 2.0)
25+
cocoapods-downloader (>= 2.1, < 3.0)
26+
cocoapods-plugins (>= 1.0.0, < 2.0)
27+
cocoapods-search (>= 1.0.0, < 2.0)
28+
cocoapods-trunk (>= 1.6.0, < 2.0)
29+
cocoapods-try (>= 1.1.0, < 2.0)
30+
colored2 (~> 3.1)
31+
escape (~> 0.0.4)
32+
fourflusher (>= 2.3.0, < 3.0)
33+
gh_inspector (~> 1.0)
34+
molinillo (~> 0.8.0)
35+
nap (~> 1.0)
36+
ruby-macho (>= 2.3.0, < 3.0)
37+
xcodeproj (>= 1.23.0, < 2.0)
38+
cocoapods-core (1.15.2)
39+
activesupport (>= 5.0, < 8)
40+
addressable (~> 2.8)
41+
algoliasearch (~> 1.0)
42+
concurrent-ruby (~> 1.1)
43+
fuzzy_match (~> 2.0.4)
44+
nap (~> 1.0)
45+
netrc (~> 0.11)
46+
public_suffix (~> 4.0)
47+
typhoeus (~> 1.0)
48+
cocoapods-deintegrate (1.0.5)
49+
cocoapods-downloader (2.1)
50+
cocoapods-plugins (1.0.0)
51+
nap
52+
cocoapods-search (1.0.1)
53+
cocoapods-trunk (1.6.0)
54+
nap (>= 0.8, < 2.0)
55+
netrc (~> 0.11)
56+
cocoapods-try (1.2.0)
57+
colored2 (3.1.2)
58+
concurrent-ruby (1.3.3)
59+
escape (0.0.4)
60+
ethon (0.18.0)
61+
ffi (>= 1.15.0)
62+
logger
63+
ffi (1.17.4)
64+
fourflusher (2.3.1)
65+
fuzzy_match (2.0.4)
66+
gh_inspector (1.1.3)
67+
httpclient (2.9.0)
68+
mutex_m
69+
i18n (1.14.8)
70+
concurrent-ruby (~> 1.0)
71+
json (2.7.6)
72+
logger (1.7.0)
73+
minitest (5.25.4)
74+
molinillo (0.8.0)
75+
mutex_m (0.3.0)
76+
nanaimo (0.3.0)
77+
nap (1.1.0)
78+
netrc (0.11.0)
79+
public_suffix (4.0.7)
80+
rexml (3.4.4)
81+
ruby-macho (2.5.1)
82+
typhoeus (1.6.0)
83+
ethon (>= 0.18.0)
84+
tzinfo (2.0.6)
85+
concurrent-ruby (~> 1.0)
86+
xcodeproj (1.25.1)
87+
CFPropertyList (>= 2.3.3, < 4.0)
88+
atomos (~> 0.1.3)
89+
claide (>= 1.0.2, < 2.0)
90+
colored2 (~> 3.1)
91+
nanaimo (~> 0.3.0)
92+
rexml (>= 3.3.6, < 4.0)
93+
zeitwerk (2.6.18)
94+
95+
PLATFORMS
96+
ruby
97+
98+
DEPENDENCIES
99+
activesupport (>= 6.1.7.5, != 7.1.0)
100+
benchmark
101+
bigdecimal
102+
cocoapods (>= 1.13, != 1.15.1, != 1.15.0)
103+
concurrent-ruby (< 1.3.4)
104+
logger
105+
mutex_m
106+
xcodeproj (< 1.26.0)
107+
108+
RUBY VERSION
109+
ruby 2.6.10p210
110+
111+
BUNDLED WITH
112+
1.17.2
Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,70 @@
11
# Knock + React Native example app
22

3-
Demonstrates [`@knocklabs/react-native`](../../packages/react-native) in a bare React Native app. For an Expo-managed example, see [`expo-example`](../expo-example).
3+
Demonstrates [`@knocklabs/react-native`](../../packages/react-native) in a bare React Native app. Mirrors the structure of the [Android](https://github.com/knocklabs/knock-android/tree/main/knock-example-app) and [iOS](https://github.com/knocklabs/ios-example-app) demos. For an Expo-managed example, see [`expo-example`](../expo-example).
4+
5+
## What this demo shows
6+
7+
Six screens, each demonstrating one piece of the SDK:
8+
9+
- **Startup** — splash before sign-in
10+
- **Sign in** — identifies the user with `KnockProvider`
11+
- **Main** — the in-app feed via the prebuilt `<NotificationFeed />` component
12+
- **Compose message** — the shape of a workflow trigger payload
13+
- **Preferences** — read and write the user's channel-type preferences via `usePreferences`
14+
- **Switch tenant** — scope the feed and preferences to a tenant
415

516
## Running locally
617

718
1. Install dependencies from the root of the monorepo.
819

9-
```sh
10-
yarn
11-
```
20+
```sh
21+
yarn
22+
```
1223

1324
2. Build the Knock packages.
1425

15-
```sh
16-
yarn build:packages
17-
```
26+
```sh
27+
yarn build:packages
28+
```
1829

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.
30+
3. Configure the app. Open [`src/config.ts`](./src/config.ts) and replace the `KNOCK_*` placeholders with values from your [Knock dashboard](https://dashboard.knock.app).
2031

2132
4. Set up your React Native development environment. See the [React Native environment setup guide](https://reactnative.dev/docs/set-up-your-environment).
2233

2334
5. For iOS, install the CocoaPods dependencies once.
2435

25-
```sh
26-
cd ios && bundle install && bundle exec pod install && cd ..
27-
```
36+
```sh
37+
cd ios && bundle install && bundle exec pod install && cd ..
38+
```
2839

2940
6. Start Metro and launch on a simulator or device.
3041

31-
```sh
32-
yarn start
42+
```sh
43+
yarn start
3344

34-
# In another terminal:
35-
yarn ios
36-
# or
37-
yarn android
38-
```
45+
# In another terminal:
46+
yarn ios
47+
# or
48+
yarn android
49+
```
3950

4051
## Configuration
4152

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).
53+
All runtime configuration lives as flat constants in [`src/config.ts`](./src/config.ts):
54+
55+
| Constant | What it is |
56+
| --- | --- |
57+
| `KNOCK_API_KEY` | Public API key. Dashboard → Developers → API keys. |
58+
| `KNOCK_USER_ID` | A test user's ID. Comes from your auth system in production. |
59+
| `KNOCK_IN_APP_CHANNEL_ID` | In-app feed channel ID. Integrations → In-app feed. |
60+
| `KNOCK_PUSH_CHANNEL_ID` | APNs/FCM push channel ID. Integrations page. |
61+
| `KNOCK_HOSTNAME` | Override for self-hosted or sandbox Knock. |
62+
| `KNOCK_TENANT_A`, `KNOCK_TENANT_B` | Tenant IDs used by the tenant switcher. |
63+
64+
## Workflow triggers
65+
66+
The Compose screen shows the *shape* of a workflow trigger payload but does not call Knock directly. Triggers require your secret API key, which must not live in a mobile app — POST the payload to a trusted backend that calls `knock.workflows.trigger` on your behalf.
67+
68+
## Push notifications
4369

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.
70+
The app wires `KnockPushNotificationProvider` so the SDK is ready to register device tokens, but the device-token registration code itself is not in this example. APNs and FCM setup require account-level configuration (certificates, Firebase project) that's out of scope here.

examples/react-native-example/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
"type:check": "tsc --noEmit"
1313
},
1414
"dependencies": {
15+
"@knocklabs/react-native": "workspace:^",
1516
"@react-navigation/native": "^7.1.33",
1617
"@react-navigation/native-stack": "^7.3.0",
1718
"react": "^19.2.5",
1819
"react-native": "^0.83.4",
20+
"react-native-gesture-handler": "~2.30.0",
1921
"react-native-safe-area-context": "~5.6.2",
2022
"react-native-screens": "~4.23.0"
2123
},
Lines changed: 82 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
1+
import {
2+
KnockFeedProvider,
3+
KnockProvider,
4+
KnockPushNotificationProvider,
5+
} from "@knocklabs/react-native";
16
import { DefaultTheme, NavigationContainer } from "@react-navigation/native";
27
import { createNativeStackNavigator } from "@react-navigation/native-stack";
38
import { StatusBar } from "react-native";
49
import { SafeAreaProvider } from "react-native-safe-area-context";
510

6-
import type { RootStackParamList } from "./navigation";
11+
import { AuthProvider, useAuth } from "./auth";
12+
import {
13+
KNOCK_API_KEY,
14+
KNOCK_HOSTNAME,
15+
KNOCK_IN_APP_CHANNEL_ID,
16+
} from "./config";
17+
import type {
18+
AuthedStackParamList,
19+
UnauthedStackParamList,
20+
} from "./navigation";
721
import MainScreen from "./screens/MainScreen";
822
import MessageComposeScreen from "./screens/MessageComposeScreen";
923
import PreferencesScreen from "./screens/PreferencesScreen";
@@ -12,7 +26,8 @@ import StartupScreen from "./screens/StartupScreen";
1226
import TenantSwitcherScreen from "./screens/TenantSwitcherScreen";
1327
import { colors } from "./theme";
1428

15-
const Stack = createNativeStackNavigator<RootStackParamList>();
29+
const UnauthedStack = createNativeStackNavigator<UnauthedStackParamList>();
30+
const AuthedStack = createNativeStackNavigator<AuthedStackParamList>();
1631

1732
const navTheme = {
1833
...DefaultTheme,
@@ -28,44 +43,75 @@ const navTheme = {
2843
},
2944
};
3045

46+
function Navigator() {
47+
const { auth } = useAuth();
48+
49+
if (!auth) {
50+
return (
51+
<UnauthedStack.Navigator initialRouteName="Startup">
52+
<UnauthedStack.Screen
53+
name="Startup"
54+
component={StartupScreen}
55+
options={{ headerShown: false }}
56+
/>
57+
<UnauthedStack.Screen
58+
name="SignIn"
59+
component={SignInScreen}
60+
options={{ title: "Sign in" }}
61+
/>
62+
</UnauthedStack.Navigator>
63+
);
64+
}
65+
66+
return (
67+
<KnockProvider
68+
apiKey={KNOCK_API_KEY}
69+
host={KNOCK_HOSTNAME}
70+
user={{ id: auth.userId }}
71+
logLevel="debug"
72+
>
73+
<KnockPushNotificationProvider>
74+
<KnockFeedProvider
75+
feedId={KNOCK_IN_APP_CHANNEL_ID}
76+
defaultFeedOptions={auth.tenant ? { tenant: auth.tenant } : undefined}
77+
>
78+
<AuthedStack.Navigator>
79+
<AuthedStack.Screen
80+
name="Main"
81+
component={MainScreen}
82+
options={{ title: "Knock", headerBackVisible: false }}
83+
/>
84+
<AuthedStack.Screen
85+
name="MessageCompose"
86+
component={MessageComposeScreen}
87+
options={{ title: "Compose message", presentation: "modal" }}
88+
/>
89+
<AuthedStack.Screen
90+
name="Preferences"
91+
component={PreferencesScreen}
92+
options={{ title: "Preferences" }}
93+
/>
94+
<AuthedStack.Screen
95+
name="TenantSwitcher"
96+
component={TenantSwitcherScreen}
97+
options={{ title: "Switch tenant", presentation: "modal" }}
98+
/>
99+
</AuthedStack.Navigator>
100+
</KnockFeedProvider>
101+
</KnockPushNotificationProvider>
102+
</KnockProvider>
103+
);
104+
}
105+
31106
export default function App() {
32107
return (
33108
<SafeAreaProvider>
34109
<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>
110+
<AuthProvider>
111+
<NavigationContainer theme={navTheme}>
112+
<Navigator />
113+
</NavigationContainer>
114+
</AuthProvider>
69115
</SafeAreaProvider>
70116
);
71117
}

0 commit comments

Comments
 (0)