Skip to content

Commit 054d4e9

Browse files
authored
feat: add the expo web platform support (#22)
* feat: add the expo web platform support - add the browser storeage - add the expo-web-browser signIn callback listener * chore: update readme * chore: update readme update readme * fix: fix readme typo fix readme typo * feat: add some comments add some comments * fix: update comments update comments
1 parent f5112ac commit 054d4e9

File tree

8 files changed

+196
-19
lines changed

8 files changed

+196
-19
lines changed

README.md

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,26 +72,50 @@ const App = () => {
7272

7373
## Run the sample app
7474

75+
> [!Note]
76+
> In terms of the redirect URI scheme, different platforms have different requirements.
77+
>
78+
> - For native platforms, a Private-Use native URI scheme is required. See [OAuth2 spec](https://datatracker.ietf.org/doc/html/rfc8252#section-8.4) for more details.
79+
> - For web platforms (SPA), an `http(s)://` scheme is required.
80+
>
81+
> You may need to register different applications in the Logto dashboard for different platforms. Make sure to configure the correct `redirectUri` and `clientId` for different platforms.
82+
7583
### Replace the `appId` and `endpoint` in `App.tsx` with your own Logto settings.
7684

7785
```tsx
7886
const endpoint = "YOUR_LOGTO_ENDPOINT";
7987
const appId = "YOUR_APP_ID";
8088
```
8189

82-
### Run using Expo Go
90+
### Development using Expo Go
8391

84-
> [!Caution]
85-
> This SDK is not compatible with "Expo Go" sandbox on Android.
86-
> Under the hood, this SDK uses `ExpoAuthSession` to handle the user authentication flow. Native deep linking is not supported in "Expo Go". For more details please refer to [deep-linking](https://docs.expo.dev/guides/deep-linking/)
87-
> Use [development-build](https://docs.expo.dev/develop/development-builds/introduction/) to test this SDK on Android.
92+
#### For iOS
8893

89-
Under the path `packages/rn-sample` run the following command.
94+
Customize the redirect URI e.g. `io.logto://callback` and pass it to the `signIn` function.
95+
96+
Run the following command under the path `packages/rn-sample`.
9097

9198
```bash
9299
pnpm dev:ios
93100
```
94101

102+
#### For web
103+
104+
Customize the redirect URI e.g. `http://localhost:19006` and pass it to the `signIn` function.
105+
106+
Run the following command under the path `packages/rn-sample`.
107+
108+
```bash
109+
pnpm dev:web
110+
```
111+
112+
#### For Android
113+
114+
> [!Caution]
115+
> This SDK is not compatible with "Expo Go" sandbox on Android.
116+
> Expo Go app by default uses `exp://` scheme for deep linking, which is not a valid private native scheme. See [OAuth2 spec](https://datatracker.ietf.org/doc/html/rfc8252#section-8.4) for more details.
117+
> For Android, Use [development-build](https://docs.expo.dev/develop/development-builds/introduction/) to test this SDK
118+
95119
### Build and run native package
96120

97121
Under the path `packages/rn-sample` run the following command.

packages/rn-sample/App.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// eslint-disable-next-line import/no-unassigned-import
22
import '@logto/rn/polyfill';
33

4-
import { LogtoProvider, useLogto, type IdTokenClaims } from '@logto/rn';
4+
import { LogtoProvider, Prompt, useLogto, type IdTokenClaims } from '@logto/rn';
55
import { StatusBar } from 'expo-status-bar';
66
import { useEffect, useState } from 'react';
77
import { Button, StyleSheet, Text, View } from 'react-native';
@@ -54,6 +54,10 @@ const App = () => {
5454
config={{
5555
endpoint,
5656
appId,
57+
// For better demonstration, override the default prompt to always show the login screen.
58+
// Default value is `Prompt.Consent`.
59+
// With `Prompt.Consent` settings, user will automatically be consented if they have a valid session.
60+
prompt: Prompt.Login,
5761
}}
5862
>
5963
<Content />

packages/rn-sample/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
"dev": "expo start",
88
"dev:android": "expo start --android",
99
"dev:ios": "expo start --ios",
10+
"dev:web": "expo start --web",
1011
"android": "expo run:android",
1112
"ios": "expo run:ios"
1213
},
1314
"dependencies": {
15+
"@expo/metro-runtime": "~3.1.3",
1416
"@logto/rn": "workspace:^",
1517
"@react-native-async-storage/async-storage": "^1.22.0",
1618
"expo": "~50.0.6",
@@ -19,7 +21,9 @@
1921
"expo-status-bar": "~1.11.1",
2022
"expo-web-browser": "^12.8.2",
2123
"react": "18.2.0",
22-
"react-native": "0.73.4"
24+
"react-dom": "18.2.0",
25+
"react-native": "0.73.4",
26+
"react-native-web": "~0.19.6"
2327
},
2428
"devDependencies": {
2529
"@babel/core": "^7.20.0",

packages/rn/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"@react-native-async-storage/async-storage": "^1.22.0",
6363
"expo-crypto": "^12.8.0",
6464
"expo-secure-store": "^12.8.1",
65-
"expo-web-browser": "^12.8.2"
65+
"expo-web-browser": "^12.8.2",
66+
"react-native": "0.73.4"
6667
}
6768
}

packages/rn/src/client.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import {
2-
type LogtoConfig,
3-
createRequester,
2+
LogtoError,
3+
Prompt,
44
StandardLogtoClient,
5+
createRequester,
56
type InteractionMode,
6-
Prompt,
7-
LogtoError,
7+
type LogtoConfig,
88
} from '@logto/client/shim';
99
import { decodeIdToken } from '@logto/js';
1010
import * as WebBrowser from 'expo-web-browser';
11+
import { Platform } from 'react-native';
1112

1213
import { LogtoNativeClientError } from './errors';
13-
import { SecureStorage } from './storage';
14+
import { BrowserStorage, SecureStorage } from './storage';
1415
import { generateCodeChallenge, generateRandomString } from './utils';
1516

1617
const issuedAtTimeTolerance = 300; // 5 minutes
@@ -39,10 +40,14 @@ export type LogtoNativeConfig = LogtoConfig & {
3940

4041
export class LogtoClient extends StandardLogtoClient {
4142
authSessionResult?: WebBrowser.WebBrowserAuthSessionResult;
42-
protected storage: SecureStorage;
43+
protected storage: SecureStorage | BrowserStorage;
4344

4445
constructor(config: LogtoNativeConfig) {
45-
const storage = new SecureStorage(`logto.${config.appId}`);
46+
const storage =
47+
Platform.OS === 'web'
48+
? new BrowserStorage(config.appId)
49+
: new SecureStorage(`logto.${config.appId}`);
50+
4651
const requester = createRequester(fetch);
4752

4853
super(

packages/rn/src/hooks.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { useCallback, useContext, useMemo } from 'react';
1+
import { maybeCompleteAuthSession } from 'expo-web-browser';
2+
import { useCallback, useContext, useEffect, useMemo } from 'react';
23

34
// eslint-disable-next-line unused-imports/no-unused-imports -- use for JSDoc
45
import type { LogtoClient } from './client';
@@ -12,6 +13,12 @@ import { LogtoContext } from './context';
1213
export const useLogto = () => {
1314
const { client, isAuthenticated, setIsAuthenticated } = useContext(LogtoContext);
1415

16+
useEffect(() => {
17+
// This is required to handle the redirect from the browser on a web-based expo app
18+
// @see {@link https://docs.expo.dev/versions/latest/sdk/webbrowser/#webbrowsermaybecompleteauthsessionoptions}
19+
maybeCompleteAuthSession();
20+
}, []);
21+
1522
const signIn = useCallback(
1623
async (redirectUri: string) => {
1724
await client.signIn(redirectUri);

packages/rn/src/storage.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { type Storage } from '@logto/client/shim';
1+
import { type Storage, type StorageKey } from '@logto/client/shim';
22
import AsyncStorage from '@react-native-async-storage/async-storage';
3+
import type { Nullable } from '@silverhand/essentials';
34
import CryptoES from 'crypto-es';
45
import * as SecureStore from 'expo-secure-store';
56

@@ -69,3 +70,50 @@ export class SecureStorage implements Storage<string> {
6970
return CryptoES.AES.decrypt(value, encryptionKey).toString(CryptoES.enc.Utf8);
7071
}
7172
}
73+
74+
const keyPrefix = `logto`;
75+
76+
/**
77+
* This is a browser storage implementation that uses `localStorage` and `sessionStorage`.
78+
*
79+
* @remarks
80+
* Forked from @logto/browser/src/storage.ts
81+
* Since `expo-secure-store` doesn't support web, we need to use the browser's native storage.
82+
* @see {@link https://docs.expo.dev/versions/latest/sdk/securestore/}
83+
*/
84+
export class BrowserStorage implements Storage<StorageKey> {
85+
constructor(public readonly appId: string) {}
86+
87+
getKey(item?: string) {
88+
if (item === undefined) {
89+
return `${keyPrefix}:${this.appId}`;
90+
}
91+
92+
return `${keyPrefix}:${this.appId}:${item}`;
93+
}
94+
95+
async getItem(key: StorageKey): Promise<Nullable<string>> {
96+
if (key === 'signInSession') {
97+
// The latter `getItem()` is for backward compatibility. Can be removed when major bump.
98+
return sessionStorage.getItem(this.getKey(key)) ?? sessionStorage.getItem(this.getKey());
99+
}
100+
101+
return localStorage.getItem(this.getKey(key));
102+
}
103+
104+
async setItem(key: StorageKey, value: string): Promise<void> {
105+
if (key === 'signInSession') {
106+
sessionStorage.setItem(this.getKey(key), value);
107+
return;
108+
}
109+
localStorage.setItem(this.getKey(key), value);
110+
}
111+
112+
async removeItem(key: StorageKey): Promise<void> {
113+
if (key === 'signInSession') {
114+
sessionStorage.removeItem(this.getKey(key));
115+
return;
116+
}
117+
localStorage.removeItem(this.getKey(key));
118+
}
119+
}

0 commit comments

Comments
 (0)