@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,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign In Component
|
|
3
|
+
* Similar to Clerk's <SignIn />
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { useHyperyAuth } from '../lib/context';
|
|
10
|
+
|
|
11
|
+
export interface SignInProps {
|
|
12
|
+
/** Button text */
|
|
13
|
+
buttonText?: string;
|
|
14
|
+
/** Custom className for styling */
|
|
15
|
+
className?: string;
|
|
16
|
+
/** Button style variant */
|
|
17
|
+
variant?: 'primary' | 'secondary' | 'outline';
|
|
18
|
+
/** Redirect path after sign in */
|
|
19
|
+
redirectTo?: string;
|
|
20
|
+
/** Show loading state */
|
|
21
|
+
loading?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Pre-built sign-in button component
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```tsx
|
|
29
|
+
* <SignIn buttonText="Sign in with Hypery" />
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function SignIn({
|
|
33
|
+
buttonText = 'Sign in with Hypery',
|
|
34
|
+
className,
|
|
35
|
+
variant = 'primary',
|
|
36
|
+
redirectTo,
|
|
37
|
+
loading,
|
|
38
|
+
}: SignInProps) {
|
|
39
|
+
const { login, isLoading } = useHyperyAuth();
|
|
40
|
+
|
|
41
|
+
const handleClick = () => {
|
|
42
|
+
if (redirectTo) {
|
|
43
|
+
sessionStorage.setItem('hypery_redirect_after_login', redirectTo);
|
|
44
|
+
}
|
|
45
|
+
login();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const isDisabled = isLoading || loading;
|
|
49
|
+
|
|
50
|
+
// Default styles based on variant
|
|
51
|
+
const getVariantStyles = () => {
|
|
52
|
+
switch (variant) {
|
|
53
|
+
case 'secondary':
|
|
54
|
+
return 'bg-gray-600 hover:bg-gray-700 text-white';
|
|
55
|
+
case 'outline':
|
|
56
|
+
return 'border-2 border-blue-600 text-blue-600 hover:bg-blue-50';
|
|
57
|
+
default:
|
|
58
|
+
return 'bg-blue-600 hover:bg-blue-700 text-white';
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const defaultStyles = `
|
|
63
|
+
inline-flex items-center justify-center
|
|
64
|
+
px-6 py-2.5
|
|
65
|
+
rounded-lg
|
|
66
|
+
font-medium
|
|
67
|
+
transition-colors
|
|
68
|
+
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
|
69
|
+
disabled:opacity-50 disabled:cursor-not-allowed
|
|
70
|
+
${getVariantStyles()}
|
|
71
|
+
`.replace(/\s+/g, ' ').trim();
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<button
|
|
75
|
+
onClick={handleClick}
|
|
76
|
+
disabled={isDisabled}
|
|
77
|
+
className={className || defaultStyles}
|
|
78
|
+
type="button"
|
|
79
|
+
>
|
|
80
|
+
{isDisabled ? 'Loading...' : buttonText}
|
|
81
|
+
</button>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SignInForm Component
|
|
3
|
+
* Embedded login form with email/password and social OAuth
|
|
4
|
+
* Authenticates directly with the Hypery without extra redirects
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import React, { useState } from 'react';
|
|
10
|
+
import { useHyperyAuth } from '../lib/context';
|
|
11
|
+
|
|
12
|
+
export interface SignInFormProps {
|
|
13
|
+
/** Custom CSS class */
|
|
14
|
+
className?: string;
|
|
15
|
+
/** Show card styling */
|
|
16
|
+
showCard?: boolean;
|
|
17
|
+
/** Show title and description */
|
|
18
|
+
showTitle?: boolean;
|
|
19
|
+
/** Custom title */
|
|
20
|
+
title?: string;
|
|
21
|
+
/** Custom description */
|
|
22
|
+
description?: string;
|
|
23
|
+
/** Show social OAuth buttons (GitHub, Google) */
|
|
24
|
+
showSocial?: boolean;
|
|
25
|
+
/** Show email/password form */
|
|
26
|
+
showEmailPassword?: boolean;
|
|
27
|
+
/** Callback after successful sign in */
|
|
28
|
+
onSuccess?: () => void;
|
|
29
|
+
/** Callback on error */
|
|
30
|
+
onError?: (error: string) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Embedded sign-in form component
|
|
35
|
+
* Provides email/password and OAuth authentication
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* <SignInForm
|
|
40
|
+
* showCard
|
|
41
|
+
* showTitle
|
|
42
|
+
* showSocial
|
|
43
|
+
* onSuccess={() => console.log('Signed in!')}
|
|
44
|
+
* />
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function SignInForm({
|
|
48
|
+
className,
|
|
49
|
+
showCard = true,
|
|
50
|
+
showTitle = true,
|
|
51
|
+
title = 'Sign in to continue',
|
|
52
|
+
description = 'Choose your preferred sign-in method',
|
|
53
|
+
showSocial = true,
|
|
54
|
+
showEmailPassword = true,
|
|
55
|
+
onSuccess,
|
|
56
|
+
onError,
|
|
57
|
+
}: SignInFormProps) {
|
|
58
|
+
const { login, signUp } = useHyperyAuth();
|
|
59
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
60
|
+
const [loadingProvider, setLoadingProvider] = useState<string | null>(null);
|
|
61
|
+
const [email, setEmail] = useState('');
|
|
62
|
+
const [password, setPassword] = useState('');
|
|
63
|
+
const [error, setError] = useState('');
|
|
64
|
+
|
|
65
|
+
// Handle OAuth sign-in with social providers
|
|
66
|
+
// This directly initiates the OAuth flow with the gateway
|
|
67
|
+
const handleSocialSignIn = (provider: 'google' | 'github') => {
|
|
68
|
+
setLoadingProvider(provider);
|
|
69
|
+
setError('');
|
|
70
|
+
try {
|
|
71
|
+
// The login() function will redirect to the gateway's OAuth flow
|
|
72
|
+
// If the user is already authenticated with the provider,
|
|
73
|
+
// they'll only need to authorize the OAuth app
|
|
74
|
+
login();
|
|
75
|
+
if (onSuccess) {
|
|
76
|
+
onSuccess();
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
const errorMessage = err instanceof Error ? err.message : `${provider} sign in failed`;
|
|
80
|
+
setError(errorMessage);
|
|
81
|
+
if (onError) {
|
|
82
|
+
onError(errorMessage);
|
|
83
|
+
}
|
|
84
|
+
setLoadingProvider(null);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const handleEmailSignIn = async (e: React.FormEvent) => {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
setIsLoading(true);
|
|
91
|
+
setError('');
|
|
92
|
+
|
|
93
|
+
// Validate
|
|
94
|
+
if (!email || !password) {
|
|
95
|
+
setError('Please enter both email and password');
|
|
96
|
+
setIsLoading(false);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (password.length < 6) {
|
|
101
|
+
setError('Password must be at least 6 characters');
|
|
102
|
+
setIsLoading(false);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// For now, redirect to OAuth flow
|
|
107
|
+
// In the future, this could call a direct auth API
|
|
108
|
+
setError('Direct email login not yet supported. Please use the sign-in button.');
|
|
109
|
+
setIsLoading(false);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const containerClasses = showCard
|
|
113
|
+
? 'bg-white rounded-lg shadow-md p-8 max-w-md mx-auto'
|
|
114
|
+
: '';
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div className={`${containerClasses} ${className || ''}`}>
|
|
118
|
+
{showTitle && (
|
|
119
|
+
<div className="text-center mb-6">
|
|
120
|
+
<h2 className="text-2xl font-bold mb-2">{title}</h2>
|
|
121
|
+
<p className="text-gray-600">{description}</p>
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{error && (
|
|
126
|
+
<div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded mb-4 text-sm">
|
|
127
|
+
{error}
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{/* Social OAuth Buttons */}
|
|
132
|
+
{showSocial && (
|
|
133
|
+
<div className="space-y-2 mb-6">
|
|
134
|
+
{/* GitHub Button */}
|
|
135
|
+
<button
|
|
136
|
+
onClick={() => handleSocialSignIn('github')}
|
|
137
|
+
disabled={!!loadingProvider}
|
|
138
|
+
className="w-full bg-[#24292F] hover:bg-[#1B1F23] text-white py-2 px-3 rounded flex items-center justify-center gap-2 text-sm font-medium transition-colors disabled:opacity-50"
|
|
139
|
+
type="button"
|
|
140
|
+
>
|
|
141
|
+
{loadingProvider === 'github' ? (
|
|
142
|
+
<span>Connecting...</span>
|
|
143
|
+
) : (
|
|
144
|
+
<>
|
|
145
|
+
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
|
146
|
+
<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" />
|
|
147
|
+
</svg>
|
|
148
|
+
<span>Continue with GitHub</span>
|
|
149
|
+
</>
|
|
150
|
+
)}
|
|
151
|
+
</button>
|
|
152
|
+
|
|
153
|
+
{/* Google Button */}
|
|
154
|
+
<button
|
|
155
|
+
onClick={() => handleSocialSignIn('google')}
|
|
156
|
+
disabled={!!loadingProvider}
|
|
157
|
+
className="w-full bg-white hover:bg-gray-50 text-gray-700 py-2 px-3 rounded flex items-center justify-center gap-2 text-sm font-medium border border-gray-300 transition-colors disabled:opacity-50"
|
|
158
|
+
type="button"
|
|
159
|
+
>
|
|
160
|
+
{loadingProvider === 'google' ? (
|
|
161
|
+
<span>Connecting...</span>
|
|
162
|
+
) : (
|
|
163
|
+
<>
|
|
164
|
+
<svg className="w-4 h-4" viewBox="0 0 24 24">
|
|
165
|
+
<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" />
|
|
166
|
+
<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" />
|
|
167
|
+
<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" />
|
|
168
|
+
<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" />
|
|
169
|
+
</svg>
|
|
170
|
+
<span>Continue with Google</span>
|
|
171
|
+
</>
|
|
172
|
+
)}
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
{/* Divider */}
|
|
178
|
+
{showSocial && showEmailPassword && (
|
|
179
|
+
<div className="relative mb-6">
|
|
180
|
+
<div className="absolute inset-0 flex items-center">
|
|
181
|
+
<div className="w-full border-t border-gray-300" />
|
|
182
|
+
</div>
|
|
183
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
184
|
+
<span className="bg-white px-3 text-gray-500">
|
|
185
|
+
Or continue with email
|
|
186
|
+
</span>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
{/* Email/Password Form */}
|
|
192
|
+
{showEmailPassword && (
|
|
193
|
+
<form onSubmit={handleEmailSignIn} className="space-y-4 mb-6">
|
|
194
|
+
<div>
|
|
195
|
+
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1.5">
|
|
196
|
+
Email
|
|
197
|
+
</label>
|
|
198
|
+
<input
|
|
199
|
+
id="email"
|
|
200
|
+
type="email"
|
|
201
|
+
value={email}
|
|
202
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
203
|
+
disabled={isLoading || !!loadingProvider}
|
|
204
|
+
className="w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed text-[15px]"
|
|
205
|
+
placeholder="you@example.com"
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div>
|
|
210
|
+
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1.5">
|
|
211
|
+
Password
|
|
212
|
+
</label>
|
|
213
|
+
<input
|
|
214
|
+
id="password"
|
|
215
|
+
type="password"
|
|
216
|
+
value={password}
|
|
217
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
218
|
+
disabled={isLoading || !!loadingProvider}
|
|
219
|
+
className="w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed text-[15px]"
|
|
220
|
+
placeholder="Enter your password"
|
|
221
|
+
/>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<button
|
|
225
|
+
type="submit"
|
|
226
|
+
disabled={isLoading || !!loadingProvider}
|
|
227
|
+
className="w-full bg-blue-600 text-white py-2.5 px-4 rounded font-medium hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-[15px]"
|
|
228
|
+
>
|
|
229
|
+
{isLoading ? 'Signing in...' : 'Sign in'}
|
|
230
|
+
</button>
|
|
231
|
+
</form>
|
|
232
|
+
)}
|
|
233
|
+
|
|
234
|
+
<div className="text-center">
|
|
235
|
+
<p className="text-sm text-gray-600">
|
|
236
|
+
Don't have an account?{' '}
|
|
237
|
+
<button
|
|
238
|
+
onClick={() => {
|
|
239
|
+
// Use signUp if available (forces account selection), otherwise fallback to login
|
|
240
|
+
if (signUp) {
|
|
241
|
+
signUp();
|
|
242
|
+
} else {
|
|
243
|
+
login();
|
|
244
|
+
}
|
|
245
|
+
}}
|
|
246
|
+
className="text-blue-600 hover:text-blue-700 font-medium"
|
|
247
|
+
type="button"
|
|
248
|
+
>
|
|
249
|
+
Sign up
|
|
250
|
+
</button>
|
|
251
|
+
</p>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SignUp Component
|
|
3
|
+
* Button to initiate the sign-up flow via OAuth
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { useHyperyAuth } from '../lib/context';
|
|
10
|
+
|
|
11
|
+
export interface SignUpProps {
|
|
12
|
+
/** Custom button text */
|
|
13
|
+
buttonText?: string;
|
|
14
|
+
/** Custom CSS class */
|
|
15
|
+
className?: string;
|
|
16
|
+
/** Button style variant */
|
|
17
|
+
variant?: 'primary' | 'secondary' | 'outline';
|
|
18
|
+
/** Callback after sign up initiated */
|
|
19
|
+
onSignUpStart?: () => void;
|
|
20
|
+
/** Custom redirect URL after sign up */
|
|
21
|
+
redirectUrl?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Sign up button component
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```tsx
|
|
29
|
+
* <SignUp buttonText="Get Started" />
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function SignUp({
|
|
33
|
+
buttonText = 'Sign up',
|
|
34
|
+
className,
|
|
35
|
+
variant = 'primary',
|
|
36
|
+
onSignUpStart,
|
|
37
|
+
redirectUrl,
|
|
38
|
+
}: SignUpProps) {
|
|
39
|
+
const { signUp, login } = useHyperyAuth();
|
|
40
|
+
|
|
41
|
+
const handleSignUp = () => {
|
|
42
|
+
if (onSignUpStart) {
|
|
43
|
+
onSignUpStart();
|
|
44
|
+
}
|
|
45
|
+
// Use signUp if available (forces account selection), otherwise fallback to login
|
|
46
|
+
if (signUp) {
|
|
47
|
+
signUp();
|
|
48
|
+
} else {
|
|
49
|
+
login();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Default styles based on variant
|
|
54
|
+
const getVariantStyles = () => {
|
|
55
|
+
switch (variant) {
|
|
56
|
+
case 'secondary':
|
|
57
|
+
return 'bg-gray-600 hover:bg-gray-700 text-white';
|
|
58
|
+
case 'outline':
|
|
59
|
+
return 'border-2 border-blue-600 text-blue-600 hover:bg-blue-50';
|
|
60
|
+
default:
|
|
61
|
+
return 'bg-blue-600 hover:bg-blue-700 text-white';
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const defaultStyles = `
|
|
66
|
+
inline-flex items-center justify-center
|
|
67
|
+
px-6 py-2.5
|
|
68
|
+
rounded-lg
|
|
69
|
+
font-medium
|
|
70
|
+
transition-colors
|
|
71
|
+
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
|
72
|
+
disabled:opacity-50 disabled:cursor-not-allowed
|
|
73
|
+
${getVariantStyles()}
|
|
74
|
+
`.replace(/\s+/g, ' ').trim();
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<button
|
|
78
|
+
onClick={handleSignUp}
|
|
79
|
+
className={className || defaultStyles}
|
|
80
|
+
type="button"
|
|
81
|
+
>
|
|
82
|
+
{buttonText}
|
|
83
|
+
</button>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ParsedError } from '../types';
|
|
4
|
+
import { formatTimeUntilReset } from '../lib/parse-error';
|
|
5
|
+
|
|
6
|
+
export interface SpendingLimitAlertProps {
|
|
7
|
+
error: ParsedError;
|
|
8
|
+
onRetry?: () => void;
|
|
9
|
+
onUpgradeLimits?: () => void;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Alert component for spending limit errors
|
|
15
|
+
* Displays a user-friendly message with retry and upgrade options
|
|
16
|
+
*/
|
|
17
|
+
export function SpendingLimitAlert({
|
|
18
|
+
error,
|
|
19
|
+
onRetry,
|
|
20
|
+
onUpgradeLimits,
|
|
21
|
+
className = '',
|
|
22
|
+
}: SpendingLimitAlertProps) {
|
|
23
|
+
if (!error.isSpendingLimit) return null;
|
|
24
|
+
|
|
25
|
+
const data = error.data as any;
|
|
26
|
+
const resetTime = data.resetsAt ? formatTimeUntilReset(data.resetsAt) : null;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className={`rounded-lg border border-orange-200 bg-orange-50 p-4 ${className}`}
|
|
31
|
+
role="alert"
|
|
32
|
+
>
|
|
33
|
+
<div className="flex items-start gap-3">
|
|
34
|
+
<div className="flex-shrink-0">
|
|
35
|
+
<svg
|
|
36
|
+
className="h-5 w-5 text-orange-400"
|
|
37
|
+
viewBox="0 0 20 20"
|
|
38
|
+
fill="currentColor"
|
|
39
|
+
aria-hidden="true"
|
|
40
|
+
>
|
|
41
|
+
<path
|
|
42
|
+
fillRule="evenodd"
|
|
43
|
+
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
|
|
44
|
+
clipRule="evenodd"
|
|
45
|
+
/>
|
|
46
|
+
</svg>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="flex-1">
|
|
49
|
+
<h3 className="text-sm font-medium text-orange-800">
|
|
50
|
+
Spending Limit Reached
|
|
51
|
+
</h3>
|
|
52
|
+
<div className="mt-2 text-sm text-orange-700">
|
|
53
|
+
<p>{error.message}</p>
|
|
54
|
+
{data.limitType && (
|
|
55
|
+
<p className="mt-1">
|
|
56
|
+
<strong className="capitalize">{data.limitType}</strong> limit:{' '}
|
|
57
|
+
{data.current || 0} / {data.limit} credits used
|
|
58
|
+
</p>
|
|
59
|
+
)}
|
|
60
|
+
{resetTime && (
|
|
61
|
+
<p className="mt-1">
|
|
62
|
+
Your limit will reset <strong>{resetTime}</strong>
|
|
63
|
+
</p>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
<div className="mt-4 flex gap-3">
|
|
67
|
+
{onRetry && (
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
onClick={onRetry}
|
|
71
|
+
className="text-sm font-medium text-orange-800 hover:text-orange-900"
|
|
72
|
+
>
|
|
73
|
+
Try again
|
|
74
|
+
</button>
|
|
75
|
+
)}
|
|
76
|
+
{onUpgradeLimits && (
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
onClick={onUpgradeLimits}
|
|
80
|
+
className="text-sm font-medium text-orange-800 hover:text-orange-900 underline"
|
|
81
|
+
>
|
|
82
|
+
Increase limits →
|
|
83
|
+
</button>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Button Component
|
|
3
|
+
* Similar to Clerk's <UserButton />
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React, { useState } from 'react';
|
|
9
|
+
import { useUser, useHyperyAuth } from '../lib/context';
|
|
10
|
+
|
|
11
|
+
export interface UserButtonProps {
|
|
12
|
+
/** Show user info */
|
|
13
|
+
showUserInfo?: boolean;
|
|
14
|
+
/** Custom className for styling */
|
|
15
|
+
className?: string;
|
|
16
|
+
/** Avatar size */
|
|
17
|
+
size?: 'sm' | 'md' | 'lg';
|
|
18
|
+
/** Render custom dropdown */
|
|
19
|
+
renderDropdown?: (user: NonNullable<ReturnType<typeof useUser>['user']>, logout: () => void) => React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* User button with avatar and dropdown menu
|
|
24
|
+
* Usually placed in the top-right corner of your layout
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <UserButton showUserInfo />
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function UserButton({
|
|
32
|
+
showUserInfo = false,
|
|
33
|
+
className,
|
|
34
|
+
size = 'md',
|
|
35
|
+
renderDropdown,
|
|
36
|
+
}: UserButtonProps) {
|
|
37
|
+
const { user, isLoading } = useUser();
|
|
38
|
+
const { logout } = useHyperyAuth();
|
|
39
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
40
|
+
|
|
41
|
+
if (isLoading || !user) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const sizeMap = {
|
|
46
|
+
sm: 'w-8 h-8 text-sm',
|
|
47
|
+
md: 'w-10 h-10 text-base',
|
|
48
|
+
lg: 'w-12 h-12 text-lg',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleLogout = async () => {
|
|
52
|
+
setIsOpen(false);
|
|
53
|
+
await logout();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className={`relative ${className || ''}`}>
|
|
58
|
+
<button
|
|
59
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
60
|
+
className={`${sizeMap[size]} rounded-full overflow-hidden bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-semibold hover:opacity-90 transition-opacity`}
|
|
61
|
+
type="button"
|
|
62
|
+
aria-label="User menu"
|
|
63
|
+
>
|
|
64
|
+
{user.image ? (
|
|
65
|
+
<img src={user.image} alt={user.name} className="w-full h-full object-cover" />
|
|
66
|
+
) : (
|
|
67
|
+
user.name.charAt(0).toUpperCase()
|
|
68
|
+
)}
|
|
69
|
+
</button>
|
|
70
|
+
|
|
71
|
+
{isOpen && (
|
|
72
|
+
<>
|
|
73
|
+
{/* Backdrop */}
|
|
74
|
+
<div
|
|
75
|
+
className="fixed inset-0 z-10"
|
|
76
|
+
onClick={() => setIsOpen(false)}
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
{/* Dropdown */}
|
|
80
|
+
<div className="absolute right-0 mt-2 w-64 bg-white rounded-lg shadow-lg z-20 py-2">
|
|
81
|
+
{renderDropdown ? (
|
|
82
|
+
renderDropdown(user, handleLogout)
|
|
83
|
+
) : (
|
|
84
|
+
<>
|
|
85
|
+
{showUserInfo && (
|
|
86
|
+
<div className="px-4 py-3 border-b">
|
|
87
|
+
<p className="font-semibold">{user.name}</p>
|
|
88
|
+
<p className="text-sm text-gray-500">{user.email}</p>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
<button
|
|
92
|
+
onClick={handleLogout}
|
|
93
|
+
className="w-full text-left px-4 py-2 hover:bg-gray-50 text-sm"
|
|
94
|
+
type="button"
|
|
95
|
+
>
|
|
96
|
+
Sign out
|
|
97
|
+
</button>
|
|
98
|
+
</>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
</>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|