@softwarepatterns/am-react 0.1.0
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/README.md +46 -0
- package/dist/AuthProvider.d.ts +59 -0
- package/dist/AuthProvider.js +114 -0
- package/dist/AuthProvider.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# `@softwarepatterns/am-react`
|
|
2
|
+
|
|
3
|
+
Headless React auth adapter for **AccountMaker (Am)**.
|
|
4
|
+
|
|
5
|
+
This package exposes the React provider and hook for an `Am` instance and its
|
|
6
|
+
current `AuthSession`.
|
|
7
|
+
|
|
8
|
+
Examples live in [`examples/`](./examples).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
bun add @softwarepatterns/am @softwarepatterns/am-react react
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { Am } from '@softwarepatterns/am';
|
|
20
|
+
import { AuthProvider, useAuth } from '@softwarepatterns/am-react';
|
|
21
|
+
|
|
22
|
+
const am = new Am({ storage: 'localStorage' });
|
|
23
|
+
|
|
24
|
+
function SessionStatus() {
|
|
25
|
+
const { isReady, session } = useAuth();
|
|
26
|
+
|
|
27
|
+
if (!isReady) return null;
|
|
28
|
+
return session ? <span>Signed in</span> : <span>Signed out</span>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function App() {
|
|
32
|
+
return (
|
|
33
|
+
<AuthProvider am={am}>
|
|
34
|
+
<SessionStatus />
|
|
35
|
+
</AuthProvider>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Exports
|
|
41
|
+
|
|
42
|
+
- `AuthProvider`
|
|
43
|
+
- `useAuth()`
|
|
44
|
+
- `useRequiredAuth()`
|
|
45
|
+
- `AuthContextValue`
|
|
46
|
+
- `AuthProviderProps`
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Am, AuthSession, AuthError } from '@softwarepatterns/am';
|
|
3
|
+
import type { SessionProfile, SessionTokens } from '@softwarepatterns/am';
|
|
4
|
+
export type AuthProviderProps = {
|
|
5
|
+
am: Am;
|
|
6
|
+
onRefresh?: (newTokens: SessionTokens) => void;
|
|
7
|
+
onProfileChange?: (profile: SessionProfile) => void;
|
|
8
|
+
onUnauthenticated?: (e: AuthError) => void;
|
|
9
|
+
onSessionChange?: (session: AuthSession | null) => void;
|
|
10
|
+
};
|
|
11
|
+
export type AuthContextValue = {
|
|
12
|
+
auth: Am;
|
|
13
|
+
session: AuthSession | null;
|
|
14
|
+
isReady: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type RequiredAuthContextValue = Omit<AuthContextValue, 'session'> & {
|
|
17
|
+
session: AuthSession;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Hook to access authentication state and actions.
|
|
21
|
+
*
|
|
22
|
+
* Must be called within <AuthProvider>.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* function Dashboard() {
|
|
27
|
+
* const { session } = useAuth();
|
|
28
|
+
*
|
|
29
|
+
* if (!session) return <Spinner />;
|
|
30
|
+
* if (!session.profile) return <Redirect to="/login" />;
|
|
31
|
+
*
|
|
32
|
+
* return <div>Welcome, {profile.identity?.displayName}</div>;
|
|
33
|
+
* }
|
|
34
|
+
* ```;
|
|
35
|
+
*/
|
|
36
|
+
export declare function useAuth(): AuthContextValue;
|
|
37
|
+
export declare function useRequiredAuth(): RequiredAuthContextValue;
|
|
38
|
+
/**
|
|
39
|
+
* Provider component that makes authentication state available throughout a
|
|
40
|
+
* React tree.
|
|
41
|
+
*
|
|
42
|
+
* Wrap your app (or auth-dependent subtree) with <AuthProvider>.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* const am = new AM({
|
|
47
|
+
* storage: 'localStorage',
|
|
48
|
+
* });
|
|
49
|
+
*
|
|
50
|
+
* function App() {
|
|
51
|
+
* return (
|
|
52
|
+
* <AuthProvider am={am}>
|
|
53
|
+
* <Router />
|
|
54
|
+
* </AuthProvider>
|
|
55
|
+
* );
|
|
56
|
+
* }
|
|
57
|
+
* ```;
|
|
58
|
+
*/
|
|
59
|
+
export declare function AuthProvider(props: React.PropsWithChildren<AuthProviderProps>): React.JSX.Element;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { useMemo, createContext, useContext, useEffect, useState, } from 'react';
|
|
3
|
+
import { Am, AuthSession, AuthError, } from '@softwarepatterns/am'; // adjust import paths
|
|
4
|
+
const AuthContext = createContext(null);
|
|
5
|
+
/**
|
|
6
|
+
* Hook to access authentication state and actions.
|
|
7
|
+
*
|
|
8
|
+
* Must be called within <AuthProvider>.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* function Dashboard() {
|
|
13
|
+
* const { session } = useAuth();
|
|
14
|
+
*
|
|
15
|
+
* if (!session) return <Spinner />;
|
|
16
|
+
* if (!session.profile) return <Redirect to="/login" />;
|
|
17
|
+
*
|
|
18
|
+
* return <div>Welcome, {profile.identity?.displayName}</div>;
|
|
19
|
+
* }
|
|
20
|
+
* ```;
|
|
21
|
+
*/
|
|
22
|
+
export function useAuth() {
|
|
23
|
+
const v = useContext(AuthContext);
|
|
24
|
+
if (!v)
|
|
25
|
+
throw new Error('useAuth must be used within <AuthProvider>');
|
|
26
|
+
return v;
|
|
27
|
+
}
|
|
28
|
+
export function useRequiredAuth() {
|
|
29
|
+
const auth = useAuth();
|
|
30
|
+
if (!auth.session) {
|
|
31
|
+
throw new Error('No session');
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
...auth,
|
|
35
|
+
session: auth.session,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Provider component that makes authentication state available throughout a
|
|
40
|
+
* React tree.
|
|
41
|
+
*
|
|
42
|
+
* Wrap your app (or auth-dependent subtree) with <AuthProvider>.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* const am = new AM({
|
|
47
|
+
* storage: 'localStorage',
|
|
48
|
+
* });
|
|
49
|
+
*
|
|
50
|
+
* function App() {
|
|
51
|
+
* return (
|
|
52
|
+
* <AuthProvider am={am}>
|
|
53
|
+
* <Router />
|
|
54
|
+
* </AuthProvider>
|
|
55
|
+
* );
|
|
56
|
+
* }
|
|
57
|
+
* ```;
|
|
58
|
+
*/
|
|
59
|
+
export function AuthProvider(props) {
|
|
60
|
+
const { children, am, onUnauthenticated, onProfileChange, onRefresh, onSessionChange, } = props;
|
|
61
|
+
const [session, setSession] = useState(null);
|
|
62
|
+
const [isReady, setIsReady] = useState(false);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const unsubs = [];
|
|
65
|
+
unsubs.push(am.on('sessionChange', (session) => {
|
|
66
|
+
const syncSessionState = async () => {
|
|
67
|
+
if (onSessionChange) {
|
|
68
|
+
await Promise.resolve(onSessionChange(session));
|
|
69
|
+
}
|
|
70
|
+
setSession(session);
|
|
71
|
+
};
|
|
72
|
+
syncSessionState().catch((error) => {
|
|
73
|
+
console.error('Failed to apply session change', error);
|
|
74
|
+
setSession(session);
|
|
75
|
+
});
|
|
76
|
+
}));
|
|
77
|
+
unsubs.push(am.on('unauthenticated', (e) => {
|
|
78
|
+
// Clear the session to prevent further refresh attempts, but do NOT
|
|
79
|
+
// update React state. The page continues to render during navigation,
|
|
80
|
+
// and clearing the session from React would cause components to crash.
|
|
81
|
+
am.session?.clear();
|
|
82
|
+
if (onUnauthenticated) {
|
|
83
|
+
onUnauthenticated(e);
|
|
84
|
+
}
|
|
85
|
+
}));
|
|
86
|
+
if (onProfileChange) {
|
|
87
|
+
unsubs.push(am.on('profileChange', onProfileChange));
|
|
88
|
+
}
|
|
89
|
+
if (onRefresh) {
|
|
90
|
+
unsubs.push(am.on('refresh', onRefresh));
|
|
91
|
+
}
|
|
92
|
+
(async () => {
|
|
93
|
+
const currentSession = am.restoreSession() ?? am.session;
|
|
94
|
+
if (currentSession?.isExpired()) {
|
|
95
|
+
try {
|
|
96
|
+
await currentSession.refresh();
|
|
97
|
+
setSession(currentSession);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
currentSession.clear();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Sync non-expired initial session
|
|
105
|
+
setSession(currentSession);
|
|
106
|
+
}
|
|
107
|
+
setIsReady(true);
|
|
108
|
+
})().catch(console.error);
|
|
109
|
+
return () => unsubs.forEach((u) => u());
|
|
110
|
+
}, [am, onProfileChange, onRefresh, onUnauthenticated, onSessionChange]);
|
|
111
|
+
const value = useMemo(() => ({ auth: am, session, isReady }), [am, session, isReady]);
|
|
112
|
+
return _jsx(AuthContext.Provider, { value: value, children: children });
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=AuthProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuthProvider.js","sourceRoot":"","sources":["../src/AuthProvider.tsx"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@softwarepatterns/am-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Headless React auth adapter for AccountMaker (Am)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"authentication",
|
|
7
|
+
"auth",
|
|
8
|
+
"react",
|
|
9
|
+
"sdk",
|
|
10
|
+
"accountmaker"
|
|
11
|
+
],
|
|
12
|
+
"author": "Software Patterns <info@softwarepatterns.com>",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/softwarepatterns/am/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/softwarepatterns/am.git",
|
|
21
|
+
"directory": "packages/am-react"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/softwarepatterns/am/tree/main/packages/am-react#readme",
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build:am": "cd ../am && bun run build",
|
|
41
|
+
"build:clean": "rm -rf dist .tsbuildinfo",
|
|
42
|
+
"build": "bun run build:clean && bun run build:am && bunx tsc -b tsconfig.build.json",
|
|
43
|
+
"prepublishOnly": "bun run build",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"test:unit": "vitest run",
|
|
46
|
+
"typecheck": "bun run build:am && bunx tsc --noEmit",
|
|
47
|
+
"lint": "oxlint"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@softwarepatterns/am": "^0.3.1"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"react": ">=19"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
57
|
+
"@testing-library/react": "^16.1.0",
|
|
58
|
+
"@types/react": "^19.1.15",
|
|
59
|
+
"@types/react-dom": "^19.1.9",
|
|
60
|
+
"jsdom": "^25.0.1",
|
|
61
|
+
"oxlint": "^1.39.0",
|
|
62
|
+
"react-dom": "^19.1.1",
|
|
63
|
+
"typescript": "^5.9.3",
|
|
64
|
+
"vitest": "^4.1.9"
|
|
65
|
+
},
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"access": "public"
|
|
68
|
+
}
|
|
69
|
+
}
|