@growsober/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 (129) hide show
  1. package/README.md +276 -0
  2. package/dist/__tests__/e2e.test.d.ts +7 -0
  3. package/dist/__tests__/e2e.test.js +472 -0
  4. package/dist/api/client.d.ts +11 -0
  5. package/dist/api/client.js +61 -0
  6. package/dist/api/mutations/admin.d.ts +167 -0
  7. package/dist/api/mutations/admin.js +326 -0
  8. package/dist/api/mutations/ambassadors.d.ts +52 -0
  9. package/dist/api/mutations/ambassadors.js +148 -0
  10. package/dist/api/mutations/auth.d.ts +267 -0
  11. package/dist/api/mutations/auth.js +332 -0
  12. package/dist/api/mutations/bookings.d.ts +59 -0
  13. package/dist/api/mutations/bookings.js +143 -0
  14. package/dist/api/mutations/event-chat.d.ts +35 -0
  15. package/dist/api/mutations/event-chat.js +147 -0
  16. package/dist/api/mutations/events.d.ts +87 -0
  17. package/dist/api/mutations/events.js +205 -0
  18. package/dist/api/mutations/grow90.d.ts +36 -0
  19. package/dist/api/mutations/grow90.js +132 -0
  20. package/dist/api/mutations/hubs.d.ts +111 -0
  21. package/dist/api/mutations/hubs.js +240 -0
  22. package/dist/api/mutations/index.d.ts +22 -0
  23. package/dist/api/mutations/index.js +39 -0
  24. package/dist/api/mutations/jack.d.ts +61 -0
  25. package/dist/api/mutations/jack.js +104 -0
  26. package/dist/api/mutations/library.d.ts +67 -0
  27. package/dist/api/mutations/library.js +168 -0
  28. package/dist/api/mutations/map.d.ts +153 -0
  29. package/dist/api/mutations/map.js +181 -0
  30. package/dist/api/mutations/matching.d.ts +130 -0
  31. package/dist/api/mutations/matching.js +204 -0
  32. package/dist/api/mutations/notifications.d.ts +63 -0
  33. package/dist/api/mutations/notifications.js +106 -0
  34. package/dist/api/mutations/offers.d.ts +26 -0
  35. package/dist/api/mutations/offers.js +47 -0
  36. package/dist/api/mutations/subscriptions.d.ts +127 -0
  37. package/dist/api/mutations/subscriptions.js +140 -0
  38. package/dist/api/mutations/support.d.ts +165 -0
  39. package/dist/api/mutations/support.js +307 -0
  40. package/dist/api/mutations/users.d.ts +211 -0
  41. package/dist/api/mutations/users.js +261 -0
  42. package/dist/api/queries/admin.d.ts +257 -0
  43. package/dist/api/queries/admin.js +320 -0
  44. package/dist/api/queries/ambassadors.d.ts +53 -0
  45. package/dist/api/queries/ambassadors.js +98 -0
  46. package/dist/api/queries/auth.d.ts +16 -0
  47. package/dist/api/queries/auth.js +25 -0
  48. package/dist/api/queries/bookings.d.ts +91 -0
  49. package/dist/api/queries/bookings.js +102 -0
  50. package/dist/api/queries/businesses.d.ts +212 -0
  51. package/dist/api/queries/businesses.js +154 -0
  52. package/dist/api/queries/event-chat.d.ts +19 -0
  53. package/dist/api/queries/event-chat.js +75 -0
  54. package/dist/api/queries/events.d.ts +322 -0
  55. package/dist/api/queries/events.js +221 -0
  56. package/dist/api/queries/grow90.d.ts +26 -0
  57. package/dist/api/queries/grow90.js +85 -0
  58. package/dist/api/queries/hubs.d.ts +165 -0
  59. package/dist/api/queries/hubs.js +143 -0
  60. package/dist/api/queries/index.d.ts +23 -0
  61. package/dist/api/queries/index.js +40 -0
  62. package/dist/api/queries/jack.d.ts +63 -0
  63. package/dist/api/queries/jack.js +92 -0
  64. package/dist/api/queries/library.d.ts +132 -0
  65. package/dist/api/queries/library.js +120 -0
  66. package/dist/api/queries/map.d.ts +216 -0
  67. package/dist/api/queries/map.js +278 -0
  68. package/dist/api/queries/matching.d.ts +136 -0
  69. package/dist/api/queries/matching.js +161 -0
  70. package/dist/api/queries/notifications.d.ts +78 -0
  71. package/dist/api/queries/notifications.js +88 -0
  72. package/dist/api/queries/offers.d.ts +91 -0
  73. package/dist/api/queries/offers.js +103 -0
  74. package/dist/api/queries/subscriptions.d.ts +56 -0
  75. package/dist/api/queries/subscriptions.js +73 -0
  76. package/dist/api/queries/support.d.ts +106 -0
  77. package/dist/api/queries/support.js +202 -0
  78. package/dist/api/queries/users.d.ts +293 -0
  79. package/dist/api/queries/users.js +370 -0
  80. package/dist/api/types.d.ts +464 -0
  81. package/dist/api/types.js +9 -0
  82. package/dist/hooks/useAuth.d.ts +5 -0
  83. package/dist/hooks/useAuth.js +39 -0
  84. package/dist/hooks/useUser.d.ts +43 -0
  85. package/dist/hooks/useUser.js +44 -0
  86. package/dist/index.d.ts +36 -0
  87. package/dist/index.js +67 -0
  88. package/package.json +62 -0
  89. package/src/__tests__/e2e.test.ts +502 -0
  90. package/src/api/client.ts +71 -0
  91. package/src/api/mutations/admin.ts +531 -0
  92. package/src/api/mutations/ambassadors.ts +185 -0
  93. package/src/api/mutations/auth.ts +350 -0
  94. package/src/api/mutations/bookings.ts +190 -0
  95. package/src/api/mutations/event-chat.ts +177 -0
  96. package/src/api/mutations/events.ts +273 -0
  97. package/src/api/mutations/grow90.ts +169 -0
  98. package/src/api/mutations/hubs.ts +385 -0
  99. package/src/api/mutations/index.ts +23 -0
  100. package/src/api/mutations/jack.ts +130 -0
  101. package/src/api/mutations/library.ts +212 -0
  102. package/src/api/mutations/map.ts +230 -0
  103. package/src/api/mutations/matching.ts +271 -0
  104. package/src/api/mutations/notifications.ts +114 -0
  105. package/src/api/mutations/offers.ts +73 -0
  106. package/src/api/mutations/subscriptions.ts +162 -0
  107. package/src/api/mutations/support.ts +390 -0
  108. package/src/api/mutations/users.ts +271 -0
  109. package/src/api/queries/admin.ts +480 -0
  110. package/src/api/queries/ambassadors.ts +139 -0
  111. package/src/api/queries/auth.ts +24 -0
  112. package/src/api/queries/bookings.ts +135 -0
  113. package/src/api/queries/businesses.ts +203 -0
  114. package/src/api/queries/event-chat.ts +78 -0
  115. package/src/api/queries/events.ts +272 -0
  116. package/src/api/queries/grow90.ts +98 -0
  117. package/src/api/queries/hubs.ts +211 -0
  118. package/src/api/queries/index.ts +24 -0
  119. package/src/api/queries/jack.ts +127 -0
  120. package/src/api/queries/library.ts +166 -0
  121. package/src/api/queries/map.ts +331 -0
  122. package/src/api/queries/matching.ts +238 -0
  123. package/src/api/queries/notifications.ts +103 -0
  124. package/src/api/queries/offers.ts +136 -0
  125. package/src/api/queries/subscriptions.ts +91 -0
  126. package/src/api/queries/support.ts +235 -0
  127. package/src/api/queries/users.ts +393 -0
  128. package/src/api/types.ts +596 -0
  129. package/src/index.ts +57 -0
@@ -0,0 +1,185 @@
1
+ import {
2
+ useMutation,
3
+ useQueryClient,
4
+ UseMutationOptions,
5
+ UseMutationResult,
6
+ } from '@tanstack/react-query';
7
+ import { getApiClient } from '../client';
8
+ import type { AmbassadorResponse, ApplyAmbassadorRequest, UpdateAmbassadorRequest } from '../types';
9
+ import { ambassadorKeys } from '../queries/ambassadors';
10
+
11
+ // ============================================================================
12
+ // MUTATION HOOKS
13
+ // ============================================================================
14
+
15
+ /**
16
+ * Apply to become an ambassador
17
+ */
18
+ export function useApplyAmbassador(
19
+ options?: Omit<
20
+ UseMutationOptions<AmbassadorResponse, Error, ApplyAmbassadorRequest>,
21
+ 'mutationFn'
22
+ >
23
+ ): UseMutationResult<AmbassadorResponse, Error, ApplyAmbassadorRequest> {
24
+ const queryClient = useQueryClient();
25
+
26
+ return useMutation({
27
+ mutationFn: async (data: ApplyAmbassadorRequest): Promise<AmbassadorResponse> => {
28
+ const client = getApiClient();
29
+ const response = await client.post('/api/v1/ambassadors/apply', data);
30
+ return response.data;
31
+ },
32
+ onSuccess: (ambassador) => {
33
+ queryClient.setQueryData(ambassadorKeys.me(), ambassador);
34
+ queryClient.invalidateQueries({ queryKey: ambassadorKeys.lists() });
35
+ },
36
+ ...options,
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Update own ambassador profile
42
+ */
43
+ export function useUpdateMyAmbassador(
44
+ options?: Omit<
45
+ UseMutationOptions<AmbassadorResponse, Error, Partial<UpdateAmbassadorRequest>>,
46
+ 'mutationFn'
47
+ >
48
+ ): UseMutationResult<AmbassadorResponse, Error, Partial<UpdateAmbassadorRequest>> {
49
+ const queryClient = useQueryClient();
50
+
51
+ return useMutation({
52
+ mutationFn: async (data: Partial<UpdateAmbassadorRequest>): Promise<AmbassadorResponse> => {
53
+ const client = getApiClient();
54
+ const response = await client.put('/api/v1/ambassadors/me', data);
55
+ return response.data;
56
+ },
57
+ onSuccess: (ambassador) => {
58
+ queryClient.setQueryData(ambassadorKeys.me(), ambassador);
59
+ queryClient.invalidateQueries({ queryKey: ambassadorKeys.lists() });
60
+ },
61
+ ...options,
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Update ambassador (admin)
67
+ */
68
+ export function useUpdateAmbassador(
69
+ options?: Omit<
70
+ UseMutationOptions<AmbassadorResponse, Error, { id: string; data: UpdateAmbassadorRequest }>,
71
+ 'mutationFn'
72
+ >
73
+ ): UseMutationResult<AmbassadorResponse, Error, { id: string; data: UpdateAmbassadorRequest }> {
74
+ const queryClient = useQueryClient();
75
+
76
+ return useMutation({
77
+ mutationFn: async ({ id, data }): Promise<AmbassadorResponse> => {
78
+ const client = getApiClient();
79
+ const response = await client.put(`/api/v1/ambassadors/${id}`, data);
80
+ return response.data;
81
+ },
82
+ onSuccess: (ambassador, { id }) => {
83
+ queryClient.setQueryData(ambassadorKeys.detail(id), ambassador);
84
+ queryClient.invalidateQueries({ queryKey: ambassadorKeys.lists() });
85
+ queryClient.invalidateQueries({ queryKey: ambassadorKeys.pending() });
86
+ queryClient.invalidateQueries({ queryKey: ambassadorKeys.leaderboard() });
87
+ },
88
+ ...options,
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Approve ambassador application (admin)
94
+ */
95
+ export function useApproveAmbassador(
96
+ options?: Omit<UseMutationOptions<AmbassadorResponse, Error, string>, 'mutationFn'>
97
+ ): UseMutationResult<AmbassadorResponse, Error, string> {
98
+ const queryClient = useQueryClient();
99
+
100
+ return useMutation({
101
+ mutationFn: async (id: string): Promise<AmbassadorResponse> => {
102
+ const client = getApiClient();
103
+ const response = await client.post(`/api/v1/ambassadors/${id}/approve`);
104
+ return response.data;
105
+ },
106
+ onSuccess: (ambassador, id) => {
107
+ queryClient.setQueryData(ambassadorKeys.detail(id), ambassador);
108
+ queryClient.invalidateQueries({ queryKey: ambassadorKeys.lists() });
109
+ queryClient.invalidateQueries({ queryKey: ambassadorKeys.pending() });
110
+ queryClient.invalidateQueries({ queryKey: ambassadorKeys.leaderboard() });
111
+ },
112
+ ...options,
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Reject ambassador application (admin)
118
+ */
119
+ export function useRejectAmbassador(
120
+ options?: Omit<UseMutationOptions<{ success: boolean }, Error, string>, 'mutationFn'>
121
+ ): UseMutationResult<{ success: boolean }, Error, string> {
122
+ const queryClient = useQueryClient();
123
+
124
+ return useMutation({
125
+ mutationFn: async (id: string): Promise<{ success: boolean }> => {
126
+ const client = getApiClient();
127
+ const response = await client.post(`/api/v1/ambassadors/${id}/reject`);
128
+ return response.data;
129
+ },
130
+ onSuccess: (_, id) => {
131
+ queryClient.removeQueries({ queryKey: ambassadorKeys.detail(id) });
132
+ queryClient.invalidateQueries({ queryKey: ambassadorKeys.lists() });
133
+ queryClient.invalidateQueries({ queryKey: ambassadorKeys.pending() });
134
+ },
135
+ ...options,
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Deactivate ambassador (admin)
141
+ */
142
+ export function useDeactivateAmbassador(
143
+ options?: Omit<UseMutationOptions<AmbassadorResponse, Error, string>, 'mutationFn'>
144
+ ): UseMutationResult<AmbassadorResponse, Error, string> {
145
+ const queryClient = useQueryClient();
146
+
147
+ return useMutation({
148
+ mutationFn: async (id: string): Promise<AmbassadorResponse> => {
149
+ const client = getApiClient();
150
+ const response = await client.post(`/api/v1/ambassadors/${id}/deactivate`);
151
+ return response.data;
152
+ },
153
+ onSuccess: (ambassador, id) => {
154
+ queryClient.setQueryData(ambassadorKeys.detail(id), ambassador);
155
+ queryClient.invalidateQueries({ queryKey: ambassadorKeys.lists() });
156
+ queryClient.invalidateQueries({ queryKey: ambassadorKeys.leaderboard() });
157
+ },
158
+ ...options,
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Add reward points to ambassador (admin)
164
+ */
165
+ export function useAddAmbassadorReward(
166
+ options?: Omit<
167
+ UseMutationOptions<{ success: boolean }, Error, { id: string; points: number; reason?: string }>,
168
+ 'mutationFn'
169
+ >
170
+ ): UseMutationResult<{ success: boolean }, Error, { id: string; points: number; reason?: string }> {
171
+ const queryClient = useQueryClient();
172
+
173
+ return useMutation({
174
+ mutationFn: async ({ id, points, reason }): Promise<{ success: boolean }> => {
175
+ const client = getApiClient();
176
+ const response = await client.post(`/api/v1/ambassadors/${id}/reward`, { points, reason });
177
+ return response.data;
178
+ },
179
+ onSuccess: (_, { id }) => {
180
+ queryClient.invalidateQueries({ queryKey: ambassadorKeys.detail(id) });
181
+ queryClient.invalidateQueries({ queryKey: ambassadorKeys.leaderboard() });
182
+ },
183
+ ...options,
184
+ });
185
+ }
@@ -0,0 +1,350 @@
1
+ /**
2
+ * Auth Mutation Hooks
3
+ *
4
+ * TanStack Query mutation hooks for authentication-related write operations.
5
+ * These hooks handle user registration, login, token refresh, and Firebase authentication.
6
+ *
7
+ * @module api/mutations/auth
8
+ */
9
+
10
+ import { useMutation, UseMutationOptions, UseMutationResult, useQueryClient } from '@tanstack/react-query';
11
+ import { getApiClient } from '../client';
12
+ import { userKeys } from '../queries/users';
13
+ import type {
14
+ RegisterRequest,
15
+ LoginRequest,
16
+ RefreshTokenRequest,
17
+ FirebaseAuthRequest,
18
+ AuthResponse,
19
+ TokenResponse,
20
+ } from '../types';
21
+
22
+ // ============================================================================
23
+ // MUTATION HOOKS
24
+ // ============================================================================
25
+
26
+ /**
27
+ * Register a new user account
28
+ *
29
+ * @description
30
+ * Creates a new user account with email/phone and password.
31
+ * Returns authentication tokens and user information upon successful registration.
32
+ *
33
+ * @endpoint POST /auth/register
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * import { useRegister } from '@growsober/sdk';
38
+ *
39
+ * function RegisterForm() {
40
+ * const { mutate: register, isPending, error } = useRegister({
41
+ * onSuccess: (data) => {
42
+ * // Store tokens securely
43
+ * await SecureStore.setItemAsync('accessToken', data.accessToken);
44
+ * await SecureStore.setItemAsync('refreshToken', data.refreshToken);
45
+ * navigation.navigate('Onboarding');
46
+ * },
47
+ * onError: (error) => {
48
+ * Alert.alert('Registration failed', error.message);
49
+ * },
50
+ * });
51
+ *
52
+ * const handleSubmit = () => {
53
+ * register({
54
+ * email: 'user@example.com',
55
+ * password: 'SecurePassword123!',
56
+ * name: 'John Doe',
57
+ * });
58
+ * };
59
+ *
60
+ * return <Button onPress={handleSubmit} disabled={isPending} />;
61
+ * }
62
+ * ```
63
+ *
64
+ * @param options - TanStack Query mutation options
65
+ * @returns TanStack Query mutation result
66
+ */
67
+ export function useRegister(
68
+ options?: Omit<UseMutationOptions<AuthResponse, Error, RegisterRequest>, 'mutationFn'>
69
+ ): UseMutationResult<AuthResponse, Error, RegisterRequest> {
70
+ const queryClient = useQueryClient();
71
+
72
+ return useMutation({
73
+ mutationFn: async (data: RegisterRequest): Promise<AuthResponse> => {
74
+ const client = getApiClient();
75
+ const response = await client.post<AuthResponse>('/auth/register', data);
76
+ return response.data;
77
+ },
78
+ onSuccess: (data, variables, context) => {
79
+ // Invalidate current user query to trigger refetch with new token
80
+ queryClient.invalidateQueries({ queryKey: userKeys.me() });
81
+ // User's onSuccess is handled by spreading options
82
+ },
83
+ ...options,
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Login with email/phone and password
89
+ *
90
+ * @description
91
+ * Authenticates an existing user with their credentials.
92
+ * Returns authentication tokens and user information upon successful login.
93
+ *
94
+ * @endpoint POST /auth/login
95
+ *
96
+ * @example
97
+ * ```tsx
98
+ * import { useLogin } from '@growsober/sdk';
99
+ *
100
+ * function LoginForm() {
101
+ * const { mutate: login, isPending, error } = useLogin({
102
+ * onSuccess: (data) => {
103
+ * // Store tokens securely
104
+ * await SecureStore.setItemAsync('accessToken', data.accessToken);
105
+ * await SecureStore.setItemAsync('refreshToken', data.refreshToken);
106
+ * navigation.navigate('Home');
107
+ * },
108
+ * });
109
+ *
110
+ * const handleSubmit = () => {
111
+ * login({
112
+ * email: 'user@example.com',
113
+ * password: 'SecurePassword123!',
114
+ * });
115
+ * };
116
+ *
117
+ * return (
118
+ * <form onSubmit={handleSubmit}>
119
+ * <input type="email" name="email" />
120
+ * <input type="password" name="password" />
121
+ * <button type="submit" disabled={isPending}>
122
+ * {isPending ? 'Logging in...' : 'Login'}
123
+ * </button>
124
+ * {error && <p className="error">{error.message}</p>}
125
+ * </form>
126
+ * );
127
+ * }
128
+ * ```
129
+ *
130
+ * @example
131
+ * Login with phone number:
132
+ * ```tsx
133
+ * login({
134
+ * phone: '+1234567890',
135
+ * password: 'SecurePassword123!',
136
+ * });
137
+ * ```
138
+ *
139
+ * @param options - TanStack Query mutation options
140
+ * @returns TanStack Query mutation result
141
+ */
142
+ export function useLogin(
143
+ options?: Omit<UseMutationOptions<AuthResponse, Error, LoginRequest>, 'mutationFn'>
144
+ ): UseMutationResult<AuthResponse, Error, LoginRequest> {
145
+ const queryClient = useQueryClient();
146
+
147
+ return useMutation({
148
+ mutationFn: async (data: LoginRequest): Promise<AuthResponse> => {
149
+ const client = getApiClient();
150
+ const response = await client.post<AuthResponse>('/auth/login', data);
151
+ return response.data;
152
+ },
153
+ onSuccess: (data, variables, context) => {
154
+ // Invalidate current user query to trigger refetch with new token
155
+ queryClient.invalidateQueries({ queryKey: userKeys.me() });
156
+ // User's onSuccess is handled by spreading options
157
+ },
158
+ ...options,
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Refresh access token using refresh token
164
+ *
165
+ * @description
166
+ * Obtains a new access token using a valid refresh token.
167
+ * Should be called when the access token expires.
168
+ *
169
+ * @endpoint POST /auth/refresh
170
+ *
171
+ * @example
172
+ * ```tsx
173
+ * import { useRefreshAuthToken } from '@growsober/sdk';
174
+ *
175
+ * function useTokenRefresh() {
176
+ * const { mutateAsync: refreshToken } = useRefreshAuthToken();
177
+ *
178
+ * const handleTokenExpired = async () => {
179
+ * const storedRefreshToken = await SecureStore.getItemAsync('refreshToken');
180
+ *
181
+ * if (!storedRefreshToken) {
182
+ * navigation.navigate('Login');
183
+ * return;
184
+ * }
185
+ *
186
+ * try {
187
+ * const { accessToken, refreshToken: newRefreshToken } = await refreshToken({
188
+ * refreshToken: storedRefreshToken,
189
+ * });
190
+ *
191
+ * // Store new tokens
192
+ * await SecureStore.setItemAsync('accessToken', accessToken);
193
+ * await SecureStore.setItemAsync('refreshToken', newRefreshToken);
194
+ * } catch (error) {
195
+ * // Refresh token is invalid or expired
196
+ * navigation.navigate('Login');
197
+ * }
198
+ * };
199
+ *
200
+ * return { handleTokenExpired };
201
+ * }
202
+ * ```
203
+ *
204
+ * @example
205
+ * Integrate with SDK configuration:
206
+ * ```tsx
207
+ * import { configureSDK } from '@growsober/sdk';
208
+ *
209
+ * configureSDK({
210
+ * baseURL: 'https://api.growsober.app',
211
+ * getAccessToken: async () => {
212
+ * return await SecureStore.getItemAsync('accessToken');
213
+ * },
214
+ * refreshAccessToken: async () => {
215
+ * const refreshToken = await SecureStore.getItemAsync('refreshToken');
216
+ * const { accessToken, refreshToken: newRefreshToken } = await fetch('/auth/refresh', {
217
+ * method: 'POST',
218
+ * body: JSON.stringify({ refreshToken }),
219
+ * }).then(r => r.json());
220
+ *
221
+ * await SecureStore.setItemAsync('accessToken', accessToken);
222
+ * await SecureStore.setItemAsync('refreshToken', newRefreshToken);
223
+ *
224
+ * return accessToken;
225
+ * },
226
+ * onUnauthorized: () => {
227
+ * navigation.navigate('Login');
228
+ * },
229
+ * });
230
+ * ```
231
+ *
232
+ * @param options - TanStack Query mutation options
233
+ * @returns TanStack Query mutation result
234
+ */
235
+ export function useRefreshAuthToken(
236
+ options?: Omit<UseMutationOptions<TokenResponse, Error, RefreshTokenRequest>, 'mutationFn'>
237
+ ): UseMutationResult<TokenResponse, Error, RefreshTokenRequest> {
238
+ return useMutation({
239
+ mutationFn: async (data: RefreshTokenRequest): Promise<TokenResponse> => {
240
+ const client = getApiClient();
241
+ const response = await client.post<TokenResponse>('/auth/refresh', data);
242
+ return response.data;
243
+ },
244
+ ...options,
245
+ });
246
+ }
247
+
248
+ /**
249
+ * Authenticate with Firebase ID token
250
+ *
251
+ * @description
252
+ * Authenticates a user using a Firebase ID token.
253
+ * Creates a new user account if one doesn't exist, or logs in an existing user.
254
+ * Returns GrowSober authentication tokens and user information.
255
+ *
256
+ * @endpoint POST /auth/firebase
257
+ *
258
+ * @example
259
+ * ```tsx
260
+ * import { useFirebaseAuth } from '@growsober/sdk';
261
+ * import { signInWithPhoneNumber } from 'firebase/auth';
262
+ *
263
+ * function PhoneAuthScreen() {
264
+ * const { mutate: firebaseAuth, isPending } = useFirebaseAuth({
265
+ * onSuccess: (data) => {
266
+ * // Store GrowSober tokens
267
+ * await SecureStore.setItemAsync('accessToken', data.accessToken);
268
+ * await SecureStore.setItemAsync('refreshToken', data.refreshToken);
269
+ *
270
+ * if (data.user.onboardingCompleted) {
271
+ * navigation.navigate('Home');
272
+ * } else {
273
+ * navigation.navigate('Onboarding');
274
+ * }
275
+ * },
276
+ * onError: (error) => {
277
+ * Alert.alert('Authentication failed', error.message);
278
+ * },
279
+ * });
280
+ *
281
+ * const handlePhoneAuth = async (phoneNumber: string) => {
282
+ * try {
283
+ * // Firebase authentication flow
284
+ * const confirmation = await signInWithPhoneNumber(auth, phoneNumber);
285
+ * const code = await promptUserForCode(); // Your UI to get verification code
286
+ * const credential = await confirmation.confirm(code);
287
+ *
288
+ * // Get Firebase ID token
289
+ * const idToken = await credential.user.getIdToken();
290
+ *
291
+ * // Authenticate with GrowSober backend
292
+ * firebaseAuth({ idToken });
293
+ * } catch (error) {
294
+ * console.error('Phone auth error:', error);
295
+ * }
296
+ * };
297
+ *
298
+ * return <PhoneInput onSubmit={handlePhoneAuth} disabled={isPending} />;
299
+ * }
300
+ * ```
301
+ *
302
+ * @example
303
+ * With Google Sign-In:
304
+ * ```tsx
305
+ * import { GoogleSignin } from '@react-native-google-signin/google-signin';
306
+ *
307
+ * const handleGoogleSignIn = async () => {
308
+ * const { idToken } = await GoogleSignin.signIn();
309
+ * firebaseAuth({ idToken });
310
+ * };
311
+ * ```
312
+ *
313
+ * @example
314
+ * With Apple Sign-In:
315
+ * ```tsx
316
+ * import * as AppleAuthentication from 'expo-apple-authentication';
317
+ *
318
+ * const handleAppleSignIn = async () => {
319
+ * const credential = await AppleAuthentication.signInAsync({
320
+ * requestedScopes: [
321
+ * AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
322
+ * AppleAuthentication.AppleAuthenticationScope.EMAIL,
323
+ * ],
324
+ * });
325
+ * firebaseAuth({ idToken: credential.identityToken });
326
+ * };
327
+ * ```
328
+ *
329
+ * @param options - TanStack Query mutation options
330
+ * @returns TanStack Query mutation result
331
+ */
332
+ export function useFirebaseAuth(
333
+ options?: Omit<UseMutationOptions<AuthResponse, Error, FirebaseAuthRequest>, 'mutationFn'>
334
+ ): UseMutationResult<AuthResponse, Error, FirebaseAuthRequest> {
335
+ const queryClient = useQueryClient();
336
+
337
+ return useMutation({
338
+ mutationFn: async (data: FirebaseAuthRequest): Promise<AuthResponse> => {
339
+ const client = getApiClient();
340
+ const response = await client.post<AuthResponse>('/auth/firebase', data);
341
+ return response.data;
342
+ },
343
+ onSuccess: (data, variables, context) => {
344
+ // Invalidate current user query to trigger refetch with new token
345
+ queryClient.invalidateQueries({ queryKey: userKeys.me() });
346
+ // User's onSuccess is handled by spreading options
347
+ },
348
+ ...options,
349
+ });
350
+ }