@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 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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"}
@@ -0,0 +1,2 @@
1
+ export { AuthProvider, useAuth, useRequiredAuth } from './AuthProvider.js';
2
+ export type { AuthContextValue, AuthProviderProps, RequiredAuthContextValue, } from './AuthProvider.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { AuthProvider, useAuth, useRequiredAuth } from './AuthProvider.js';
2
+ //# sourceMappingURL=index.js.map
@@ -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
+ }