@pylonsync/react-native 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/auth.ts +180 -0
- package/src/index.ts +16 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.8.
|
|
6
|
+
"version": "0.8.3",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "src/index.ts",
|
|
9
9
|
"types": "src/index.ts",
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
"check": "tsc -p tsconfig.json --noEmit"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@pylonsync/react": "0.8.
|
|
16
|
-
"@pylonsync/sdk": "0.8.
|
|
17
|
-
"@pylonsync/sync": "0.8.
|
|
15
|
+
"@pylonsync/react": "0.8.3",
|
|
16
|
+
"@pylonsync/sdk": "0.8.3",
|
|
17
|
+
"@pylonsync/sync": "0.8.3"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
20
|
"react": ">=19",
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth for React Native apps.
|
|
3
|
+
*
|
|
4
|
+
* `@pylonsync/client`'s helpers persist the session through
|
|
5
|
+
* `window.localStorage`, which does not exist on a device. These talk to
|
|
6
|
+
* the same server routes and persist through the AsyncStorage bridge that
|
|
7
|
+
* `init()` registered, which is where the sync engine reads the token.
|
|
8
|
+
*
|
|
9
|
+
* Every function returns after the local session is updated, so the next
|
|
10
|
+
* `useAuth()` / `db.useQuery` sees the new identity.
|
|
11
|
+
*/
|
|
12
|
+
import { db, getBaseUrl, getReactStorage, storageKey } from "@pylonsync/react";
|
|
13
|
+
|
|
14
|
+
export interface Session {
|
|
15
|
+
token: string;
|
|
16
|
+
user_id?: string | null;
|
|
17
|
+
expires_at?: string | number | null;
|
|
18
|
+
guest?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class AuthError extends Error {
|
|
22
|
+
readonly code: string;
|
|
23
|
+
readonly status: number;
|
|
24
|
+
constructor(code: string, message: string, status: number) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "AuthError";
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.status = status;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function currentToken(): string | null {
|
|
33
|
+
return getReactStorage().get(storageKey("token")) ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function request<T>(
|
|
37
|
+
method: "GET" | "POST" | "DELETE",
|
|
38
|
+
path: string,
|
|
39
|
+
body?: unknown,
|
|
40
|
+
): Promise<T> {
|
|
41
|
+
const headers: Record<string, string> = { accept: "application/json" };
|
|
42
|
+
if (body !== undefined) headers["content-type"] = "application/json";
|
|
43
|
+
const token = currentToken();
|
|
44
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
45
|
+
const res = await fetch(`${getBaseUrl()}${path}`, {
|
|
46
|
+
method,
|
|
47
|
+
headers,
|
|
48
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
49
|
+
});
|
|
50
|
+
const data = (await res.json().catch(() => ({}))) as T & {
|
|
51
|
+
error?: { code?: string; message?: string };
|
|
52
|
+
};
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
throw new AuthError(
|
|
55
|
+
data.error?.code ?? `HTTP_${res.status}`,
|
|
56
|
+
data.error?.message ?? "Request failed",
|
|
57
|
+
res.status,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return data;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Store a session token and tell the sync engine the identity changed.
|
|
65
|
+
* The engine wipes the previous identity's replica and re-pulls.
|
|
66
|
+
*/
|
|
67
|
+
export function persistSession(session: Session): void {
|
|
68
|
+
getReactStorage().set(storageKey("token"), session.token);
|
|
69
|
+
void db.sync.notifySessionChanged();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Forget the local session without calling the server. */
|
|
73
|
+
export function clearSession(): void {
|
|
74
|
+
getReactStorage().remove(storageKey("token"));
|
|
75
|
+
void db.sync.notifySessionChanged();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Start as an anonymous guest so the app is usable before sign-up. A
|
|
80
|
+
* later sign-in through any method below merges the guest's rows into
|
|
81
|
+
* the real account (the server's anonymous merge).
|
|
82
|
+
*/
|
|
83
|
+
export async function guestSession(): Promise<Session> {
|
|
84
|
+
const session = await request<Session>("POST", "/api/auth/guest", {});
|
|
85
|
+
persistSession(session);
|
|
86
|
+
return session;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Email a 6-digit code. Rate-limited server-side. */
|
|
90
|
+
export function sendEmailCode(email: string): Promise<{ sent: boolean }> {
|
|
91
|
+
return request<{ sent: boolean }>("POST", "/api/auth/magic/send", { email });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Redeem the emailed code for a session. */
|
|
95
|
+
export async function verifyEmailCode(email: string, code: string): Promise<Session> {
|
|
96
|
+
const session = await request<Session>("POST", "/api/auth/magic/verify", {
|
|
97
|
+
email,
|
|
98
|
+
code,
|
|
99
|
+
});
|
|
100
|
+
persistSession(session);
|
|
101
|
+
return session;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function passwordLogin(email: string, password: string): Promise<Session> {
|
|
105
|
+
const session = await request<Session>("POST", "/api/auth/password/login", {
|
|
106
|
+
email,
|
|
107
|
+
password,
|
|
108
|
+
});
|
|
109
|
+
persistSession(session);
|
|
110
|
+
return session;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function passwordRegister(
|
|
114
|
+
email: string,
|
|
115
|
+
password: string,
|
|
116
|
+
): Promise<Session> {
|
|
117
|
+
const session = await request<Session>("POST", "/api/auth/password/register", {
|
|
118
|
+
email,
|
|
119
|
+
password,
|
|
120
|
+
});
|
|
121
|
+
persistSession(session);
|
|
122
|
+
return session;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Sign in with an id_token from the platform SDK (Sign in with Apple,
|
|
127
|
+
* Google Sign-In). The server verifies it against the provider's keys
|
|
128
|
+
* (`POST /api/auth/native/<provider>`); the server needs the app's
|
|
129
|
+
* bundle id / client id in PYLON_APPLE_NATIVE_CLIENT_IDS or
|
|
130
|
+
* PYLON_GOOGLE_NATIVE_CLIENT_IDS. `name` is forwarded for Apple, which
|
|
131
|
+
* only reveals it to the app on the first sign-in.
|
|
132
|
+
*/
|
|
133
|
+
export async function nativeSignIn(
|
|
134
|
+
provider: "apple" | "google",
|
|
135
|
+
idToken: string,
|
|
136
|
+
name?: string | null,
|
|
137
|
+
): Promise<Session> {
|
|
138
|
+
const session = await request<Session>("POST", `/api/auth/native/${provider}`, {
|
|
139
|
+
id_token: idToken,
|
|
140
|
+
...(name ? { name } : {}),
|
|
141
|
+
});
|
|
142
|
+
persistSession(session);
|
|
143
|
+
return session;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface Me {
|
|
147
|
+
user_id: string | null;
|
|
148
|
+
tenant_id?: string | null;
|
|
149
|
+
is_admin?: boolean;
|
|
150
|
+
roles?: string[];
|
|
151
|
+
guest?: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The server's view of the current session, or null when signed out. */
|
|
155
|
+
export async function getMe(): Promise<Me | null> {
|
|
156
|
+
if (!currentToken()) return null;
|
|
157
|
+
try {
|
|
158
|
+
return await request<Me>("GET", "/api/auth/me");
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Revoke the server session and forget it locally. */
|
|
165
|
+
export async function signOut(): Promise<void> {
|
|
166
|
+
if (currentToken()) {
|
|
167
|
+
await request("POST", "/api/auth/logout", {}).catch(() => undefined);
|
|
168
|
+
}
|
|
169
|
+
clearSession();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Delete the account: the server revokes every session and API key,
|
|
174
|
+
* removes the provider links, and deletes the User row. Required by the
|
|
175
|
+
* App Store for any app that offers account creation.
|
|
176
|
+
*/
|
|
177
|
+
export async function deleteAccount(): Promise<void> {
|
|
178
|
+
await request("DELETE", "/api/auth/account");
|
|
179
|
+
clearSession();
|
|
180
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,22 @@ import { AsyncStorageReplicaPersistence } from "./replica-persistence";
|
|
|
29
29
|
export * from "@pylonsync/react";
|
|
30
30
|
|
|
31
31
|
// React Native specific.
|
|
32
|
+
export {
|
|
33
|
+
AuthError,
|
|
34
|
+
clearSession,
|
|
35
|
+
deleteAccount,
|
|
36
|
+
getMe,
|
|
37
|
+
guestSession,
|
|
38
|
+
nativeSignIn,
|
|
39
|
+
passwordLogin,
|
|
40
|
+
passwordRegister,
|
|
41
|
+
persistSession,
|
|
42
|
+
sendEmailCode,
|
|
43
|
+
signOut,
|
|
44
|
+
verifyEmailCode,
|
|
45
|
+
type Me,
|
|
46
|
+
type Session,
|
|
47
|
+
} from "./auth";
|
|
32
48
|
export { useNetworkStatus } from "./useNetworkStatus";
|
|
33
49
|
export type { NetworkStatus } from "./useNetworkStatus";
|
|
34
50
|
export { AsyncStorageReplicaPersistence } from "./replica-persistence";
|