@hyperyai/sdk 1.0.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/LICENSE +13 -0
- package/README.md +391 -0
- package/dist/components/AuthButton.d.ts +51 -0
- package/dist/components/AuthButton.js +60 -0
- package/dist/components/AuthModal.d.ts +20 -0
- package/dist/components/AuthModal.js +62 -0
- package/dist/components/BuyButton.d.ts +47 -0
- package/dist/components/BuyButton.js +74 -0
- package/dist/components/ErrorBoundary.d.ts +13 -0
- package/dist/components/ErrorBoundary.js +24 -0
- package/dist/components/HyperyModals.d.ts +30 -0
- package/dist/components/HyperyModals.js +30 -0
- package/dist/components/InsufficientCreditsAlert.d.ts +11 -0
- package/dist/components/InsufficientCreditsAlert.js +12 -0
- package/dist/components/ModernAuthForm.d.ts +52 -0
- package/dist/components/ModernAuthForm.js +83 -0
- package/dist/components/RestrictionModal.d.ts +43 -0
- package/dist/components/RestrictionModal.js +167 -0
- package/dist/components/SignIn.d.ts +26 -0
- package/dist/components/SignIn.js +47 -0
- package/dist/components/SignInForm.d.ts +41 -0
- package/dist/components/SignInForm.js +86 -0
- package/dist/components/SignUp.d.ts +26 -0
- package/dist/components/SignUp.js +52 -0
- package/dist/components/SpendingLimitAlert.d.ts +12 -0
- package/dist/components/SpendingLimitAlert.js +14 -0
- package/dist/components/UserButton.d.ts +26 -0
- package/dist/components/UserButton.js +35 -0
- package/dist/components/UserProfile.d.ts +22 -0
- package/dist/components/UserProfile.js +26 -0
- package/dist/components/WorkspaceSwitcher.d.ts +29 -0
- package/dist/components/WorkspaceSwitcher.js +66 -0
- package/dist/components/control.d.ts +64 -0
- package/dist/components/control.js +89 -0
- package/dist/components/ui/dialog.d.ts +19 -0
- package/dist/components/ui/dialog.js +53 -0
- package/dist/hooks/index.d.ts +22 -0
- package/dist/hooks/index.js +23 -0
- package/dist/hooks/useBuyerWallet.d.ts +37 -0
- package/dist/hooks/useBuyerWallet.js +66 -0
- package/dist/hooks/useCheckout.d.ts +27 -0
- package/dist/hooks/useCheckout.js +246 -0
- package/dist/hooks/useError.d.ts +19 -0
- package/dist/hooks/useError.js +36 -0
- package/dist/hooks/useMarketplace.d.ts +50 -0
- package/dist/hooks/useMarketplace.js +69 -0
- package/dist/hooks/useMemberships.d.ts +71 -0
- package/dist/hooks/useMemberships.js +138 -0
- package/dist/hooks/useWallet.d.ts +62 -0
- package/dist/hooks/useWallet.js +123 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +52 -0
- package/dist/lib/context.d.ts +50 -0
- package/dist/lib/context.js +409 -0
- package/dist/lib/oauth.d.ts +49 -0
- package/dist/lib/oauth.js +149 -0
- package/dist/lib/parse-error.d.ts +45 -0
- package/dist/lib/parse-error.js +185 -0
- package/dist/lib/parse-stream.d.ts +43 -0
- package/dist/lib/parse-stream.js +89 -0
- package/dist/lib/popup.d.ts +42 -0
- package/dist/lib/popup.js +80 -0
- package/dist/lib/storage.d.ts +43 -0
- package/dist/lib/storage.js +125 -0
- package/dist/lib/utils.d.ts +8 -0
- package/dist/lib/utils.js +11 -0
- package/dist/types/index.d.ts +191 -0
- package/dist/types/index.js +4 -0
- package/package.json +78 -0
- package/src/components/AuthButton.tsx +123 -0
- package/src/components/AuthModal.tsx +240 -0
- package/src/components/BuyButton.tsx +150 -0
- package/src/components/ErrorBoundary.tsx +97 -0
- package/src/components/HyperyModals.tsx +83 -0
- package/src/components/InsufficientCreditsAlert.tsx +73 -0
- package/src/components/ModernAuthForm.tsx +322 -0
- package/src/components/RestrictionModal.tsx +373 -0
- package/src/components/SignIn.tsx +84 -0
- package/src/components/SignInForm.tsx +256 -0
- package/src/components/SignUp.tsx +86 -0
- package/src/components/SpendingLimitAlert.tsx +91 -0
- package/src/components/UserButton.tsx +106 -0
- package/src/components/UserProfile.tsx +106 -0
- package/src/components/WorkspaceSwitcher.tsx +199 -0
- package/src/components/control.tsx +133 -0
- package/src/components/ui/dialog.tsx +151 -0
- package/src/hooks/index.ts +65 -0
- package/src/hooks/useBuyerWallet.ts +117 -0
- package/src/hooks/useCheckout.ts +312 -0
- package/src/hooks/useError.ts +60 -0
- package/src/hooks/useMarketplace.ts +129 -0
- package/src/hooks/useMemberships.ts +203 -0
- package/src/hooks/useWallet.ts +170 -0
- package/src/index.ts +147 -0
- package/src/lib/context.tsx +478 -0
- package/src/lib/oauth.ts +215 -0
- package/src/lib/parse-error.ts +224 -0
- package/src/lib/parse-stream.ts +121 -0
- package/src/lib/popup.ts +98 -0
- package/src/lib/storage.ts +140 -0
- package/src/lib/utils.ts +14 -0
- package/src/types/index.ts +216 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hypery Auth Types
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* How interactive auth / card-entry steps are presented.
|
|
6
|
+
* - `popup` — a centered popup (seamless; stays on your page). PayPal/Firebase-style.
|
|
7
|
+
* - `redirect` — a full-page redirect that resumes the flow on return. Robust everywhere.
|
|
8
|
+
* - `auto` — popup on desktop, redirect on mobile / when a popup is blocked.
|
|
9
|
+
*/
|
|
10
|
+
export type InteractionMode = 'auto' | 'popup' | 'redirect';
|
|
11
|
+
/** The concrete mode after resolving `auto` for the current environment. */
|
|
12
|
+
export type ResolvedMode = 'popup' | 'redirect';
|
|
13
|
+
/** Result of a popup-based login attempt. */
|
|
14
|
+
export interface PopupAuthResult {
|
|
15
|
+
/** Authentication succeeded and the user is now signed in. */
|
|
16
|
+
ok: boolean;
|
|
17
|
+
/** The browser blocked the popup — caller should fall back to redirect. */
|
|
18
|
+
blocked: boolean;
|
|
19
|
+
/** The user closed the popup without completing auth. */
|
|
20
|
+
cancelled: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Custom branding shared by AuthButton / AuthModal / ModernAuthForm.
|
|
24
|
+
*/
|
|
25
|
+
export interface BrandingConfig {
|
|
26
|
+
/** URL to logo image */
|
|
27
|
+
logo?: string;
|
|
28
|
+
/** Your app name */
|
|
29
|
+
appName?: string;
|
|
30
|
+
/** Hex color (e.g., '#8b5cf6') */
|
|
31
|
+
primaryColor?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface HyperyAuthConfig {
|
|
34
|
+
/** OAuth Client ID */
|
|
35
|
+
clientId: string;
|
|
36
|
+
/** Redirect URI after auth */
|
|
37
|
+
redirectUri: string;
|
|
38
|
+
/** Hypery base URL */
|
|
39
|
+
gatewayUrl: string;
|
|
40
|
+
/** OAuth scopes to request */
|
|
41
|
+
scopes?: string[];
|
|
42
|
+
/** Storage type for tokens */
|
|
43
|
+
storage?: 'localStorage' | 'sessionStorage' | 'memory';
|
|
44
|
+
/**
|
|
45
|
+
* How interactive auth / card-entry steps in the auth+charge flow are shown.
|
|
46
|
+
* Defaults to `auto` (popup on desktop, redirect on mobile / blocked popups).
|
|
47
|
+
* For popup mode, `redirectUri` must be same-origin as your app.
|
|
48
|
+
*/
|
|
49
|
+
interactionMode?: InteractionMode;
|
|
50
|
+
/**
|
|
51
|
+
* Called when an {@link AuthContextValue.authenticatedFetch} request stays 401
|
|
52
|
+
* after a single silent-refresh retry (server-revoked token / suspended app).
|
|
53
|
+
* Use for custom re-auth handling; if omitted, `authRequired` is set so
|
|
54
|
+
* `<HyperyModals>` can auto-open the auth modal.
|
|
55
|
+
*/
|
|
56
|
+
onUnauthorized?: () => void;
|
|
57
|
+
/**
|
|
58
|
+
* Called when an {@link AuthContextValue.authenticatedFetch} request returns a
|
|
59
|
+
* billing restriction (402/429). Receives the classified error; if omitted,
|
|
60
|
+
* `restriction` is set so `<HyperyModals>` can auto-open the funds modal.
|
|
61
|
+
*/
|
|
62
|
+
onRestricted?: (error: ParsedError) => void;
|
|
63
|
+
}
|
|
64
|
+
export interface User {
|
|
65
|
+
id: string;
|
|
66
|
+
email: string;
|
|
67
|
+
name: string;
|
|
68
|
+
image?: string;
|
|
69
|
+
}
|
|
70
|
+
export interface AuthTokens {
|
|
71
|
+
accessToken: string;
|
|
72
|
+
refreshToken: string;
|
|
73
|
+
expiresIn: number;
|
|
74
|
+
tokenType: string;
|
|
75
|
+
}
|
|
76
|
+
export interface AuthState {
|
|
77
|
+
user: User | null;
|
|
78
|
+
isAuthenticated: boolean;
|
|
79
|
+
isLoading: boolean;
|
|
80
|
+
error: string | null;
|
|
81
|
+
isLoggingOut?: boolean;
|
|
82
|
+
}
|
|
83
|
+
export interface AuthContextValue extends AuthState {
|
|
84
|
+
login: () => Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Log in via a centered popup (no full-page redirect). Resolves once the popup
|
|
87
|
+
* completes: `ok` on success, `blocked` if the browser blocked the popup (fall
|
|
88
|
+
* back to `login()`), `cancelled` if the user closed it. Used by the seamless
|
|
89
|
+
* auth+charge flow (see `useCheckout`).
|
|
90
|
+
*/
|
|
91
|
+
loginPopup: () => Promise<PopupAuthResult>;
|
|
92
|
+
/** The interaction mode resolved for this environment (popup vs redirect). */
|
|
93
|
+
interactionMode: ResolvedMode;
|
|
94
|
+
/** The redirectUri this provider was configured with. */
|
|
95
|
+
redirectUri: string;
|
|
96
|
+
signUp?: () => Promise<void>;
|
|
97
|
+
logout: () => Promise<void>;
|
|
98
|
+
refreshAuth: () => Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Get a valid access token, refreshing if the local expiry clock says so.
|
|
101
|
+
* Pass `forceRefresh` to refresh even when the clock says valid (used by the
|
|
102
|
+
* 401 retry path). Concurrent calls share ONE in-flight refresh (single-flight).
|
|
103
|
+
*/
|
|
104
|
+
getAccessToken: (forceRefresh?: boolean) => Promise<string | null>;
|
|
105
|
+
/** OAuth clientId this provider was configured with. */
|
|
106
|
+
clientId: string;
|
|
107
|
+
/** Gateway base URL this provider was configured with. */
|
|
108
|
+
gatewayUrl: string;
|
|
109
|
+
/**
|
|
110
|
+
* fetch() that injects the bearer token and drives the turnkey modal UX:
|
|
111
|
+
* 401 → one silent-refresh retry, then `authRequired` (+ `onUnauthorized`);
|
|
112
|
+
* 402/429 → `restriction` is set (+ `onRestricted`). Always returns the
|
|
113
|
+
* Response so the caller can still read the body.
|
|
114
|
+
*/
|
|
115
|
+
authenticatedFetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
116
|
+
/** Current billing restriction from the last authenticatedFetch, if any. */
|
|
117
|
+
restriction: ParsedError | null;
|
|
118
|
+
/** Clear the current restriction (e.g. after the funds modal closes). */
|
|
119
|
+
clearRestriction: () => void;
|
|
120
|
+
/** True when re-auth is required (401 survived a refresh retry). */
|
|
121
|
+
authRequired: boolean;
|
|
122
|
+
/** Clear the re-auth requirement (e.g. after the auth modal closes). */
|
|
123
|
+
clearAuthRequired: () => void;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Error response types from Hypery API
|
|
127
|
+
*/
|
|
128
|
+
export interface SpendingLimitErrorData {
|
|
129
|
+
code: 'SPENDING_LIMIT_EXCEEDED';
|
|
130
|
+
message: string;
|
|
131
|
+
type: 'spending_limit_error';
|
|
132
|
+
limitType: 'daily' | 'monthly' | 'total';
|
|
133
|
+
limit: number;
|
|
134
|
+
current: number;
|
|
135
|
+
requested: number;
|
|
136
|
+
resetsAt?: string;
|
|
137
|
+
}
|
|
138
|
+
export interface InsufficientCreditsErrorData {
|
|
139
|
+
code: 'INSUFFICIENT_CREDITS';
|
|
140
|
+
message: string;
|
|
141
|
+
type: 'insufficient_credits_error';
|
|
142
|
+
available: number;
|
|
143
|
+
required: number;
|
|
144
|
+
}
|
|
145
|
+
export interface PaymentMethodRequiredErrorData {
|
|
146
|
+
code: 'PAYMENT_METHOD_REQUIRED';
|
|
147
|
+
message: string;
|
|
148
|
+
type: 'payment_method_required_error';
|
|
149
|
+
}
|
|
150
|
+
export interface PaymentDeclinedErrorData {
|
|
151
|
+
code: 'PAYMENT_DECLINED';
|
|
152
|
+
message: string;
|
|
153
|
+
type: 'payment_declined_error';
|
|
154
|
+
reason?: string;
|
|
155
|
+
}
|
|
156
|
+
export interface AuthenticationErrorData {
|
|
157
|
+
code: 'UNAUTHENTICATED';
|
|
158
|
+
message: string;
|
|
159
|
+
type: 'authentication_error';
|
|
160
|
+
}
|
|
161
|
+
export interface GenericErrorData {
|
|
162
|
+
code: string;
|
|
163
|
+
message: string;
|
|
164
|
+
type?: string;
|
|
165
|
+
[key: string]: any;
|
|
166
|
+
}
|
|
167
|
+
export type ErrorData = SpendingLimitErrorData | InsufficientCreditsErrorData | PaymentMethodRequiredErrorData | PaymentDeclinedErrorData | AuthenticationErrorData | GenericErrorData;
|
|
168
|
+
export interface ErrorResponse {
|
|
169
|
+
error: ErrorData;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Parsed error information
|
|
173
|
+
*/
|
|
174
|
+
export interface ParsedError {
|
|
175
|
+
code: string;
|
|
176
|
+
message: string;
|
|
177
|
+
type?: string;
|
|
178
|
+
/** HTTP status, when the caller attaches it (used as a classification fallback). */
|
|
179
|
+
status?: number;
|
|
180
|
+
isSpendingLimit: boolean;
|
|
181
|
+
isInsufficientCredits: boolean;
|
|
182
|
+
isPaymentMethodRequired: boolean;
|
|
183
|
+
isPaymentDeclined: boolean;
|
|
184
|
+
/** True for an authentication failure (401 / UNAUTHENTICATED) — drive re-auth. */
|
|
185
|
+
isAuth: boolean;
|
|
186
|
+
/** True for a scope/permission denial (403 / PERMISSION_DENIED / INSUFFICIENT_SCOPE). */
|
|
187
|
+
isPermissionDenied: boolean;
|
|
188
|
+
/** True for a rate-limit denial (429 / RATE_LIMITED). */
|
|
189
|
+
isRateLimit: boolean;
|
|
190
|
+
data: ErrorData;
|
|
191
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hyperyai/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Drop-in authentication, checkout, and error handling for apps built on Hypery",
|
|
5
|
+
"license": "PIF",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/hyperyai/hypery-sdk.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/hyperyai/hypery-sdk#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/hyperyai/hypery-sdk/issues"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"source": "./src/index.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"files": [
|
|
26
|
+
"src/",
|
|
27
|
+
"dist/",
|
|
28
|
+
"README.md",
|
|
29
|
+
"!src/**/__tests__"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.json",
|
|
36
|
+
"test": "bun test src",
|
|
37
|
+
"prepack": "npm run build",
|
|
38
|
+
"prepublishOnly": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@radix-ui/react-dialog": "^1.0.5",
|
|
42
|
+
"clsx": "^2.1.0",
|
|
43
|
+
"lucide-react": "^0.541.0",
|
|
44
|
+
"tailwind-merge": "^3.6.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
48
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
52
|
+
"@semantic-release/github": "^12.0.6",
|
|
53
|
+
"@semantic-release/npm": "^13.1.5",
|
|
54
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
55
|
+
"@types/node": "^26.1.0",
|
|
56
|
+
"@types/react": "^19.0.0",
|
|
57
|
+
"@types/react-dom": "^19.0.0",
|
|
58
|
+
"react": "^19.0.0",
|
|
59
|
+
"react-dom": "^19.0.0",
|
|
60
|
+
"semantic-release": "^25.0.3",
|
|
61
|
+
"typescript": "^6.0.3"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=22"
|
|
65
|
+
},
|
|
66
|
+
"keywords": [
|
|
67
|
+
"hypery",
|
|
68
|
+
"auth",
|
|
69
|
+
"authentication",
|
|
70
|
+
"oauth",
|
|
71
|
+
"pkce",
|
|
72
|
+
"react",
|
|
73
|
+
"checkout",
|
|
74
|
+
"error-handling",
|
|
75
|
+
"spending-limits",
|
|
76
|
+
"credits"
|
|
77
|
+
]
|
|
78
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthButton Component
|
|
3
|
+
* Simple button that triggers authentication modal
|
|
4
|
+
* Drop this anywhere in your app for instant auth
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import React, { useState } from 'react';
|
|
10
|
+
import { AuthModal } from './AuthModal';
|
|
11
|
+
|
|
12
|
+
export interface AuthButtonProps {
|
|
13
|
+
/** Button text */
|
|
14
|
+
children?: React.ReactNode;
|
|
15
|
+
/** Button variant */
|
|
16
|
+
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
|
|
17
|
+
/** Button size */
|
|
18
|
+
size?: 'sm' | 'md' | 'lg';
|
|
19
|
+
/** Custom className */
|
|
20
|
+
className?: string;
|
|
21
|
+
/** Initial auth mode */
|
|
22
|
+
mode?: 'signin' | 'signup';
|
|
23
|
+
/** Callback after successful auth */
|
|
24
|
+
onSuccess?: () => void;
|
|
25
|
+
/** Show social login options */
|
|
26
|
+
showSocial?: boolean;
|
|
27
|
+
/** Show email/password form */
|
|
28
|
+
showEmailPassword?: boolean;
|
|
29
|
+
/** Custom branding for modal */
|
|
30
|
+
branding?: {
|
|
31
|
+
logo?: string;
|
|
32
|
+
appName?: string;
|
|
33
|
+
primaryColor?: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Button that opens authentication modal
|
|
39
|
+
* Perfect for navbar, landing pages, or anywhere you need auth
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* // Simple usage
|
|
44
|
+
* <AuthButton>Sign In</AuthButton>
|
|
45
|
+
*
|
|
46
|
+
* // With customization
|
|
47
|
+
* <AuthButton
|
|
48
|
+
* variant="primary"
|
|
49
|
+
* mode="signup"
|
|
50
|
+
* onSuccess={() => router.push('/dashboard')}
|
|
51
|
+
* branding={{ appName: 'My App', primaryColor: '#8b5cf6' }}
|
|
52
|
+
* >
|
|
53
|
+
* Get Started
|
|
54
|
+
* </AuthButton>
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function AuthButton({
|
|
58
|
+
children = 'Sign In',
|
|
59
|
+
variant = 'primary',
|
|
60
|
+
size = 'md',
|
|
61
|
+
className = '',
|
|
62
|
+
mode = 'signin',
|
|
63
|
+
onSuccess,
|
|
64
|
+
showSocial = true,
|
|
65
|
+
showEmailPassword = true,
|
|
66
|
+
branding,
|
|
67
|
+
}: AuthButtonProps) {
|
|
68
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
69
|
+
|
|
70
|
+
const sizeClasses = {
|
|
71
|
+
sm: 'px-4 py-2 text-sm',
|
|
72
|
+
md: 'px-6 py-2.5 text-base',
|
|
73
|
+
lg: 'px-8 py-3 text-lg',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const variantClasses = {
|
|
77
|
+
primary: 'bg-purple-600 hover:bg-purple-700 text-white shadow-md hover:shadow-lg',
|
|
78
|
+
secondary: 'bg-gray-600 hover:bg-gray-700 text-white shadow-md hover:shadow-lg',
|
|
79
|
+
outline: 'border-2 border-purple-600 text-purple-600 hover:bg-purple-50 dark:hover:bg-purple-950/30',
|
|
80
|
+
ghost: 'hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handleClick = () => {
|
|
84
|
+
console.log('AuthButton clicked! Opening modal...', { isOpen, mode });
|
|
85
|
+
setIsOpen(true);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
console.log('AuthButton render:', { isOpen, mode, variant, size });
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<>
|
|
92
|
+
<button
|
|
93
|
+
onClick={handleClick}
|
|
94
|
+
type="button"
|
|
95
|
+
className={`
|
|
96
|
+
${sizeClasses[size]}
|
|
97
|
+
${variantClasses[variant]}
|
|
98
|
+
rounded-lg font-medium transition-all
|
|
99
|
+
${className}
|
|
100
|
+
`}
|
|
101
|
+
>
|
|
102
|
+
{children}
|
|
103
|
+
</button>
|
|
104
|
+
|
|
105
|
+
<AuthModal
|
|
106
|
+
isOpen={isOpen}
|
|
107
|
+
onClose={() => {
|
|
108
|
+
console.log('AuthModal closing...');
|
|
109
|
+
setIsOpen(false);
|
|
110
|
+
}}
|
|
111
|
+
initialMode={mode}
|
|
112
|
+
onSuccess={() => {
|
|
113
|
+
setIsOpen(false);
|
|
114
|
+
onSuccess?.();
|
|
115
|
+
}}
|
|
116
|
+
showSocial={showSocial}
|
|
117
|
+
showEmailPassword={showEmailPassword}
|
|
118
|
+
branding={branding}
|
|
119
|
+
/>
|
|
120
|
+
</>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthModal Component
|
|
3
|
+
* Modal dialog for authentication using shadcn Dialog
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React, { useState } from 'react';
|
|
9
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
|
|
10
|
+
import { useHyperyAuth } from '../lib/context';
|
|
11
|
+
|
|
12
|
+
export interface AuthModalProps {
|
|
13
|
+
isOpen: boolean;
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
initialMode?: 'signin' | 'signup';
|
|
16
|
+
onSuccess?: () => void;
|
|
17
|
+
onError?: (error: string) => void;
|
|
18
|
+
showSocial?: boolean;
|
|
19
|
+
showEmailPassword?: boolean;
|
|
20
|
+
branding?: {
|
|
21
|
+
logo?: string;
|
|
22
|
+
appName?: string;
|
|
23
|
+
primaryColor?: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function AuthModal({
|
|
28
|
+
isOpen,
|
|
29
|
+
onClose,
|
|
30
|
+
initialMode = 'signin',
|
|
31
|
+
onSuccess,
|
|
32
|
+
onError,
|
|
33
|
+
showSocial = true,
|
|
34
|
+
showEmailPassword = false,
|
|
35
|
+
branding,
|
|
36
|
+
}: AuthModalProps) {
|
|
37
|
+
|
|
38
|
+
const { login } = useHyperyAuth();
|
|
39
|
+
const [mode, setMode] = useState<'signin' | 'signup'>(initialMode);
|
|
40
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
41
|
+
const [loadingProvider, setLoadingProvider] = useState<string | null>(null);
|
|
42
|
+
const [email, setEmail] = useState('');
|
|
43
|
+
const [password, setPassword] = useState('');
|
|
44
|
+
const [name, setName] = useState('');
|
|
45
|
+
const [error, setError] = useState('');
|
|
46
|
+
|
|
47
|
+
const handleSocialSignIn = async (provider: 'google' | 'github') => {
|
|
48
|
+
console.log('[AuthModal] Social button clicked:', provider);
|
|
49
|
+
setLoadingProvider(provider);
|
|
50
|
+
setError('');
|
|
51
|
+
try {
|
|
52
|
+
console.log('[AuthModal] Calling login()...');
|
|
53
|
+
await login();
|
|
54
|
+
console.log('[AuthModal] Login completed');
|
|
55
|
+
if (onSuccess) {
|
|
56
|
+
onSuccess();
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error('[AuthModal] Login failed:', err);
|
|
60
|
+
const errorMessage = err instanceof Error ? err.message : `${provider} sign in failed`;
|
|
61
|
+
setError(errorMessage);
|
|
62
|
+
if (onError) {
|
|
63
|
+
onError(errorMessage);
|
|
64
|
+
}
|
|
65
|
+
setLoadingProvider(null);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const handleEmailSubmit = (e: React.FormEvent) => {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
setError('');
|
|
72
|
+
setIsLoading(true);
|
|
73
|
+
|
|
74
|
+
if (!email || !password) {
|
|
75
|
+
setError('Please fill in all fields');
|
|
76
|
+
setIsLoading(false);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (mode === 'signup' && !name) {
|
|
81
|
+
setError('Please enter your name');
|
|
82
|
+
setIsLoading(false);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setError('Direct email authentication coming soon. Please use social sign-in.');
|
|
87
|
+
setIsLoading(false);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const primaryColor = branding?.primaryColor || '#8b5cf6';
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
|
94
|
+
<DialogContent className="sm:max-w-md bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
|
95
|
+
<DialogHeader>
|
|
96
|
+
{branding?.logo && (
|
|
97
|
+
<img src={branding.logo} alt="Logo" className="h-12 mx-auto mb-4" />
|
|
98
|
+
)}
|
|
99
|
+
<DialogTitle className="text-center text-2xl text-gray-900 dark:text-white">
|
|
100
|
+
{mode === 'signin' ? 'Welcome back' : 'Create your account'}
|
|
101
|
+
</DialogTitle>
|
|
102
|
+
<DialogDescription className="text-center text-sm text-gray-600 dark:text-gray-400">
|
|
103
|
+
{mode === 'signin'
|
|
104
|
+
? `Sign in to ${branding?.appName || 'continue'}`
|
|
105
|
+
: `Get started with ${branding?.appName || 'your account'}`
|
|
106
|
+
}
|
|
107
|
+
</DialogDescription>
|
|
108
|
+
</DialogHeader>
|
|
109
|
+
|
|
110
|
+
<div className="space-y-6 py-4">
|
|
111
|
+
{/* Error message */}
|
|
112
|
+
{error && (
|
|
113
|
+
<div className="p-3 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200 text-sm">
|
|
114
|
+
{error}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{/* Social buttons */}
|
|
119
|
+
{showSocial && (
|
|
120
|
+
<div className="space-y-2">
|
|
121
|
+
<button
|
|
122
|
+
onClick={() => handleSocialSignIn('google')}
|
|
123
|
+
disabled={!!loadingProvider}
|
|
124
|
+
className="w-full bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white py-2 px-3 rounded flex items-center justify-center gap-2 text-sm font-medium transition-all disabled:opacity-50"
|
|
125
|
+
>
|
|
126
|
+
{loadingProvider === 'google' ? (
|
|
127
|
+
<div className="w-4 h-4 border-2 border-gray-300 border-t-transparent rounded-full animate-spin" />
|
|
128
|
+
) : (
|
|
129
|
+
<svg className="w-4 h-4" viewBox="0 0 24 24">
|
|
130
|
+
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
|
|
131
|
+
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
|
|
132
|
+
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
|
|
133
|
+
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
|
|
134
|
+
</svg>
|
|
135
|
+
)}
|
|
136
|
+
<span>Continue with Google</span>
|
|
137
|
+
</button>
|
|
138
|
+
|
|
139
|
+
<button
|
|
140
|
+
onClick={() => handleSocialSignIn('github')}
|
|
141
|
+
disabled={!!loadingProvider}
|
|
142
|
+
className="w-full bg-gray-900 dark:bg-gray-800 hover:bg-gray-800 dark:hover:bg-gray-750 text-white py-2 px-3 rounded flex items-center justify-center gap-2 text-sm font-medium transition-all disabled:opacity-50"
|
|
143
|
+
>
|
|
144
|
+
{loadingProvider === 'github' ? (
|
|
145
|
+
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
146
|
+
) : (
|
|
147
|
+
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
|
148
|
+
<path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" />
|
|
149
|
+
</svg>
|
|
150
|
+
)}
|
|
151
|
+
<span>Continue with GitHub</span>
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{/* Divider */}
|
|
157
|
+
{showSocial && showEmailPassword && (
|
|
158
|
+
<div className="relative">
|
|
159
|
+
<div className="absolute inset-0 flex items-center">
|
|
160
|
+
<div className="w-full border-t border-gray-300 dark:border-gray-700" />
|
|
161
|
+
</div>
|
|
162
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
163
|
+
<span className="bg-white dark:bg-gray-900 px-2 text-gray-500 dark:text-gray-400">
|
|
164
|
+
Or continue with email
|
|
165
|
+
</span>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
{/* Email/Password form */}
|
|
171
|
+
{showEmailPassword && (
|
|
172
|
+
<form onSubmit={handleEmailSubmit} className="space-y-4">
|
|
173
|
+
{mode === 'signup' && (
|
|
174
|
+
<div className="space-y-2">
|
|
175
|
+
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Full name</label>
|
|
176
|
+
<input
|
|
177
|
+
type="text"
|
|
178
|
+
value={name}
|
|
179
|
+
onChange={(e) => setName(e.target.value)}
|
|
180
|
+
className="w-full px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
|
181
|
+
placeholder="John Doe"
|
|
182
|
+
disabled={isLoading}
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
<div className="space-y-2">
|
|
188
|
+
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Email</label>
|
|
189
|
+
<input
|
|
190
|
+
type="email"
|
|
191
|
+
value={email}
|
|
192
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
193
|
+
className="w-full px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
|
194
|
+
placeholder="you@example.com"
|
|
195
|
+
disabled={isLoading}
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div className="space-y-2">
|
|
200
|
+
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Password</label>
|
|
201
|
+
<input
|
|
202
|
+
type="password"
|
|
203
|
+
value={password}
|
|
204
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
205
|
+
className="w-full px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
|
206
|
+
placeholder="••••••••"
|
|
207
|
+
disabled={isLoading}
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<button
|
|
212
|
+
type="submit"
|
|
213
|
+
disabled={isLoading}
|
|
214
|
+
className="w-full py-2 px-4 rounded-md font-medium text-white transition-all"
|
|
215
|
+
style={{ backgroundColor: primaryColor }}
|
|
216
|
+
>
|
|
217
|
+
{isLoading ? 'Processing...' : mode === 'signin' ? 'Sign in' : 'Create account'}
|
|
218
|
+
</button>
|
|
219
|
+
</form>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
{/* Toggle mode */}
|
|
223
|
+
<div className="text-center text-sm">
|
|
224
|
+
<span className="text-gray-600 dark:text-gray-400">
|
|
225
|
+
{mode === 'signin' ? "Don't have an account?" : 'Already have an account?'}
|
|
226
|
+
</span>
|
|
227
|
+
{' '}
|
|
228
|
+
<button
|
|
229
|
+
onClick={() => setMode(mode === 'signin' ? 'signup' : 'signin')}
|
|
230
|
+
className="font-medium hover:underline"
|
|
231
|
+
style={{ color: primaryColor }}
|
|
232
|
+
>
|
|
233
|
+
{mode === 'signin' ? 'Sign up' : 'Sign in'}
|
|
234
|
+
</button>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</DialogContent>
|
|
238
|
+
</Dialog>
|
|
239
|
+
);
|
|
240
|
+
}
|