@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.
Files changed (102) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +391 -0
  3. package/dist/components/AuthButton.d.ts +51 -0
  4. package/dist/components/AuthButton.js +60 -0
  5. package/dist/components/AuthModal.d.ts +20 -0
  6. package/dist/components/AuthModal.js +62 -0
  7. package/dist/components/BuyButton.d.ts +47 -0
  8. package/dist/components/BuyButton.js +74 -0
  9. package/dist/components/ErrorBoundary.d.ts +13 -0
  10. package/dist/components/ErrorBoundary.js +24 -0
  11. package/dist/components/HyperyModals.d.ts +30 -0
  12. package/dist/components/HyperyModals.js +30 -0
  13. package/dist/components/InsufficientCreditsAlert.d.ts +11 -0
  14. package/dist/components/InsufficientCreditsAlert.js +12 -0
  15. package/dist/components/ModernAuthForm.d.ts +52 -0
  16. package/dist/components/ModernAuthForm.js +83 -0
  17. package/dist/components/RestrictionModal.d.ts +43 -0
  18. package/dist/components/RestrictionModal.js +167 -0
  19. package/dist/components/SignIn.d.ts +26 -0
  20. package/dist/components/SignIn.js +47 -0
  21. package/dist/components/SignInForm.d.ts +41 -0
  22. package/dist/components/SignInForm.js +86 -0
  23. package/dist/components/SignUp.d.ts +26 -0
  24. package/dist/components/SignUp.js +52 -0
  25. package/dist/components/SpendingLimitAlert.d.ts +12 -0
  26. package/dist/components/SpendingLimitAlert.js +14 -0
  27. package/dist/components/UserButton.d.ts +26 -0
  28. package/dist/components/UserButton.js +35 -0
  29. package/dist/components/UserProfile.d.ts +22 -0
  30. package/dist/components/UserProfile.js +26 -0
  31. package/dist/components/WorkspaceSwitcher.d.ts +29 -0
  32. package/dist/components/WorkspaceSwitcher.js +66 -0
  33. package/dist/components/control.d.ts +64 -0
  34. package/dist/components/control.js +89 -0
  35. package/dist/components/ui/dialog.d.ts +19 -0
  36. package/dist/components/ui/dialog.js +53 -0
  37. package/dist/hooks/index.d.ts +22 -0
  38. package/dist/hooks/index.js +23 -0
  39. package/dist/hooks/useBuyerWallet.d.ts +37 -0
  40. package/dist/hooks/useBuyerWallet.js +66 -0
  41. package/dist/hooks/useCheckout.d.ts +27 -0
  42. package/dist/hooks/useCheckout.js +246 -0
  43. package/dist/hooks/useError.d.ts +19 -0
  44. package/dist/hooks/useError.js +36 -0
  45. package/dist/hooks/useMarketplace.d.ts +50 -0
  46. package/dist/hooks/useMarketplace.js +69 -0
  47. package/dist/hooks/useMemberships.d.ts +71 -0
  48. package/dist/hooks/useMemberships.js +138 -0
  49. package/dist/hooks/useWallet.d.ts +62 -0
  50. package/dist/hooks/useWallet.js +123 -0
  51. package/dist/index.d.ts +55 -0
  52. package/dist/index.js +52 -0
  53. package/dist/lib/context.d.ts +50 -0
  54. package/dist/lib/context.js +409 -0
  55. package/dist/lib/oauth.d.ts +49 -0
  56. package/dist/lib/oauth.js +149 -0
  57. package/dist/lib/parse-error.d.ts +45 -0
  58. package/dist/lib/parse-error.js +185 -0
  59. package/dist/lib/parse-stream.d.ts +43 -0
  60. package/dist/lib/parse-stream.js +89 -0
  61. package/dist/lib/popup.d.ts +42 -0
  62. package/dist/lib/popup.js +80 -0
  63. package/dist/lib/storage.d.ts +43 -0
  64. package/dist/lib/storage.js +125 -0
  65. package/dist/lib/utils.d.ts +8 -0
  66. package/dist/lib/utils.js +11 -0
  67. package/dist/types/index.d.ts +191 -0
  68. package/dist/types/index.js +4 -0
  69. package/package.json +78 -0
  70. package/src/components/AuthButton.tsx +123 -0
  71. package/src/components/AuthModal.tsx +240 -0
  72. package/src/components/BuyButton.tsx +150 -0
  73. package/src/components/ErrorBoundary.tsx +97 -0
  74. package/src/components/HyperyModals.tsx +83 -0
  75. package/src/components/InsufficientCreditsAlert.tsx +73 -0
  76. package/src/components/ModernAuthForm.tsx +322 -0
  77. package/src/components/RestrictionModal.tsx +373 -0
  78. package/src/components/SignIn.tsx +84 -0
  79. package/src/components/SignInForm.tsx +256 -0
  80. package/src/components/SignUp.tsx +86 -0
  81. package/src/components/SpendingLimitAlert.tsx +91 -0
  82. package/src/components/UserButton.tsx +106 -0
  83. package/src/components/UserProfile.tsx +106 -0
  84. package/src/components/WorkspaceSwitcher.tsx +199 -0
  85. package/src/components/control.tsx +133 -0
  86. package/src/components/ui/dialog.tsx +151 -0
  87. package/src/hooks/index.ts +65 -0
  88. package/src/hooks/useBuyerWallet.ts +117 -0
  89. package/src/hooks/useCheckout.ts +312 -0
  90. package/src/hooks/useError.ts +60 -0
  91. package/src/hooks/useMarketplace.ts +129 -0
  92. package/src/hooks/useMemberships.ts +203 -0
  93. package/src/hooks/useWallet.ts +170 -0
  94. package/src/index.ts +147 -0
  95. package/src/lib/context.tsx +478 -0
  96. package/src/lib/oauth.ts +215 -0
  97. package/src/lib/parse-error.ts +224 -0
  98. package/src/lib/parse-stream.ts +121 -0
  99. package/src/lib/popup.ts +98 -0
  100. package/src/lib/storage.ts +140 -0
  101. package/src/lib/utils.ts +14 -0
  102. 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
+