@wopr-network/platform-ui-core 1.1.13 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-ui-core",
3
- "version": "1.1.13",
3
+ "version": "1.3.0",
4
4
  "description": "Brand-agnostic AI agent platform UI — deploy as any brand via env vars",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,6 +36,7 @@ import {
36
36
  TableHeader,
37
37
  TableRow,
38
38
  } from "@/components/ui/table";
39
+ import { useIsAdminOrOwner } from "@/hooks/use-my-org-role";
39
40
  import type { Organization, OrgMember } from "@/lib/api";
40
41
  import {
41
42
  changeRole,
@@ -58,6 +59,7 @@ export default function OrgPage() {
58
59
  const routerRef = useRef(router);
59
60
  routerRef.current = router;
60
61
  const [org, setOrg] = useState<Organization | null>(null);
62
+ const isAdminOrOwner = useIsAdminOrOwner(org);
61
63
  const [loading, setLoading] = useState(true);
62
64
  const [saveError, setSaveError] = useState<string | null>(null);
63
65
 
@@ -219,82 +221,102 @@ export default function OrgPage() {
219
221
  )}
220
222
  </AnimatePresence>
221
223
 
222
- <Card>
223
- <CardHeader>
224
- <CardTitle>Organization Details</CardTitle>
225
- <CardDescription>Update your organization name and billing email</CardDescription>
226
- </CardHeader>
227
- <CardContent>
228
- <form onSubmit={handleSave} className="flex flex-col gap-4">
229
- <div className="flex flex-col gap-2">
230
- <Label htmlFor="org-name">Organization name</Label>
231
- <Input
232
- id="org-name"
233
- value={orgName}
234
- onChange={(e) => setOrgName(e.target.value)}
235
- required
236
- />
237
- </div>
238
- <div className="flex flex-col gap-2">
239
- <Label htmlFor="org-slug">Slug</Label>
240
- <Input
241
- id="org-slug"
242
- value={orgSlug}
243
- onChange={(e) => setOrgSlug(e.target.value)}
244
- placeholder="my-org"
245
- />
246
- </div>
247
- <div className="flex flex-col gap-2">
248
- <Label htmlFor="billing-email">Billing email</Label>
249
- <Input
250
- id="billing-email"
251
- type="email"
252
- value={billingEmail}
253
- onChange={(e) => setBillingEmail(e.target.value)}
254
- required
255
- />
256
- </div>
257
- <AnimatePresence>
258
- {saveMsg && (
259
- <motion.p
260
- initial={{ opacity: 0, y: -4 }}
261
- animate={{ opacity: 1, y: 0 }}
262
- exit={{ opacity: 0, y: -4 }}
263
- transition={{ duration: 0.15 }}
264
- className="text-sm text-terminal"
265
- >
266
- {saveMsg}
267
- </motion.p>
268
- )}
269
- </AnimatePresence>
270
- <Button type="submit" variant="terminal" className="w-fit" disabled={saving}>
271
- <AnimatePresence mode="wait">
272
- {saveSuccess ? (
273
- <motion.span
274
- key="success"
275
- initial={{ opacity: 0, scale: 0.8 }}
276
- animate={{ opacity: 1, scale: 1 }}
277
- exit={{ opacity: 0, scale: 0.8 }}
278
- className="flex items-center gap-1"
224
+ {isAdminOrOwner ? (
225
+ <Card>
226
+ <CardHeader>
227
+ <CardTitle>Organization Details</CardTitle>
228
+ <CardDescription>Update your organization name and billing email</CardDescription>
229
+ </CardHeader>
230
+ <CardContent>
231
+ <form onSubmit={handleSave} className="flex flex-col gap-4">
232
+ <div className="flex flex-col gap-2">
233
+ <Label htmlFor="org-name">Organization name</Label>
234
+ <Input
235
+ id="org-name"
236
+ value={orgName}
237
+ onChange={(e) => setOrgName(e.target.value)}
238
+ required
239
+ />
240
+ </div>
241
+ <div className="flex flex-col gap-2">
242
+ <Label htmlFor="org-slug">Slug</Label>
243
+ <Input
244
+ id="org-slug"
245
+ value={orgSlug}
246
+ onChange={(e) => setOrgSlug(e.target.value)}
247
+ placeholder="my-org"
248
+ />
249
+ </div>
250
+ <div className="flex flex-col gap-2">
251
+ <Label htmlFor="billing-email">Billing email</Label>
252
+ <Input
253
+ id="billing-email"
254
+ type="email"
255
+ value={billingEmail}
256
+ onChange={(e) => setBillingEmail(e.target.value)}
257
+ required
258
+ />
259
+ </div>
260
+ <AnimatePresence>
261
+ {saveMsg && (
262
+ <motion.p
263
+ initial={{ opacity: 0, y: -4 }}
264
+ animate={{ opacity: 1, y: 0 }}
265
+ exit={{ opacity: 0, y: -4 }}
266
+ transition={{ duration: 0.15 }}
267
+ className="text-sm text-terminal"
279
268
  >
280
- <CheckIcon className="size-4" />
281
- Saved
282
- </motion.span>
283
- ) : (
284
- <motion.span
285
- key="save"
286
- initial={{ opacity: 0 }}
287
- animate={{ opacity: 1 }}
288
- exit={{ opacity: 0 }}
289
- >
290
- {saving ? "Saving..." : "Save changes"}
291
- </motion.span>
269
+ {saveMsg}
270
+ </motion.p>
292
271
  )}
293
272
  </AnimatePresence>
294
- </Button>
295
- </form>
296
- </CardContent>
297
- </Card>
273
+ <Button type="submit" variant="terminal" className="w-fit" disabled={saving}>
274
+ <AnimatePresence mode="wait">
275
+ {saveSuccess ? (
276
+ <motion.span
277
+ key="success"
278
+ initial={{ opacity: 0, scale: 0.8 }}
279
+ animate={{ opacity: 1, scale: 1 }}
280
+ exit={{ opacity: 0, scale: 0.8 }}
281
+ className="flex items-center gap-1"
282
+ >
283
+ <CheckIcon className="size-4" />
284
+ Saved
285
+ </motion.span>
286
+ ) : (
287
+ <motion.span
288
+ key="save"
289
+ initial={{ opacity: 0 }}
290
+ animate={{ opacity: 1 }}
291
+ exit={{ opacity: 0 }}
292
+ >
293
+ {saving ? "Saving..." : "Save changes"}
294
+ </motion.span>
295
+ )}
296
+ </AnimatePresence>
297
+ </Button>
298
+ </form>
299
+ </CardContent>
300
+ </Card>
301
+ ) : (
302
+ <Card>
303
+ <CardHeader>
304
+ <CardTitle>Organization Details</CardTitle>
305
+ <CardDescription>You are a member of this organization</CardDescription>
306
+ </CardHeader>
307
+ <CardContent>
308
+ <div className="flex flex-col gap-2 text-sm">
309
+ <div>
310
+ <span className="font-medium text-muted-foreground">Name:</span> {org.name}
311
+ </div>
312
+ <div>
313
+ <span className="font-medium text-muted-foreground">Slug:</span>{" "}
314
+ {org.slug ?? <span className="text-muted-foreground italic">not set</span>}
315
+ </div>
316
+ </div>
317
+ </CardContent>
318
+ </Card>
319
+ )}
298
320
 
299
321
  <Separator />
300
322
 
@@ -326,7 +348,7 @@ export default function OrgPage() {
326
348
  {org.members.length} member{org.members.length !== 1 ? "s" : ""}
327
349
  </p>
328
350
  </div>
329
- <InviteDialog orgId={org.id} onInvited={load} />
351
+ {isAdminOrOwner && <InviteDialog orgId={org.id} onInvited={load} />}
330
352
  </div>
331
353
 
332
354
  <div className="rounded-md border overflow-x-auto">
@@ -337,7 +359,7 @@ export default function OrgPage() {
337
359
  <TableHead>Email</TableHead>
338
360
  <TableHead>Role</TableHead>
339
361
  <TableHead>Joined</TableHead>
340
- <TableHead className="w-[160px]" />
362
+ {isAdminOrOwner && <TableHead className="w-[160px]" />}
341
363
  </TableRow>
342
364
  </TableHeader>
343
365
  <TableBody>
@@ -355,31 +377,33 @@ export default function OrgPage() {
355
377
  year: "numeric",
356
378
  })}
357
379
  </TableCell>
358
- <TableCell>
359
- {member.role !== "owner" && (
360
- <div className="flex gap-1">
361
- <ChangeRoleSelect
362
- currentRole={member.role}
363
- onChangeRole={(role) => handleChangeRole(member.userId, role)}
364
- />
365
- <TransferDialog
366
- memberName={member.name}
367
- onTransfer={() => handleTransfer(member.userId)}
368
- />
369
- <RemoveMemberDialog
370
- memberName={member.name}
371
- onRemove={() => handleRemove(member.userId)}
372
- />
373
- </div>
374
- )}
375
- </TableCell>
380
+ {isAdminOrOwner && (
381
+ <TableCell>
382
+ {member.role !== "owner" && (
383
+ <div className="flex gap-1">
384
+ <ChangeRoleSelect
385
+ currentRole={member.role}
386
+ onChangeRole={(role) => handleChangeRole(member.userId, role)}
387
+ />
388
+ <TransferDialog
389
+ memberName={member.name}
390
+ onTransfer={() => handleTransfer(member.userId)}
391
+ />
392
+ <RemoveMemberDialog
393
+ memberName={member.name}
394
+ onRemove={() => handleRemove(member.userId)}
395
+ />
396
+ </div>
397
+ )}
398
+ </TableCell>
399
+ )}
376
400
  </TableRow>
377
401
  ))}
378
402
  </TableBody>
379
403
  </Table>
380
404
  </div>
381
405
 
382
- {org.invites.length > 0 && (
406
+ {isAdminOrOwner && org.invites.length > 0 && (
383
407
  <>
384
408
  <Separator />
385
409
  <div>
@@ -1,11 +1,16 @@
1
1
  "use client";
2
2
 
3
3
  import { motion } from "framer-motion";
4
- import { Bitcoin } from "lucide-react";
5
- import { useState } from "react";
4
+ import { Bitcoin, Check, CircleDollarSign, Copy } from "lucide-react";
5
+ import { useCallback, useEffect, useRef, useState } from "react";
6
+ import { Badge } from "@/components/ui/badge";
6
7
  import { Button } from "@/components/ui/button";
7
8
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
8
- import { createCryptoCheckout } from "@/lib/api";
9
+ import {
10
+ createCryptoCheckout,
11
+ createStablecoinCheckout,
12
+ type StablecoinCheckoutResult,
13
+ } from "@/lib/api";
9
14
  import { cn } from "@/lib/utils";
10
15
  import { isAllowedRedirectUrl } from "@/lib/validate-redirect-url";
11
16
 
@@ -16,12 +21,109 @@ const CRYPTO_AMOUNTS = [
16
21
  { value: 100, label: "$100" },
17
22
  ];
18
23
 
24
+ const STABLECOIN_TOKENS = [
25
+ { token: "USDC", label: "USDC", chain: "base", chainLabel: "Base" },
26
+ { token: "USDT", label: "USDT", chain: "base", chainLabel: "Base" },
27
+ { token: "DAI", label: "DAI", chain: "base", chainLabel: "Base" },
28
+ ];
29
+
30
+ type PaymentMethod = "btc" | "stablecoin";
31
+
32
+ function CopyButton({ text }: { text: string }) {
33
+ const [copied, setCopied] = useState(false);
34
+
35
+ const handleCopy = useCallback(() => {
36
+ navigator.clipboard.writeText(text);
37
+ setCopied(true);
38
+ setTimeout(() => setCopied(false), 2000);
39
+ }, [text]);
40
+
41
+ return (
42
+ <Button variant="ghost" size="sm" onClick={handleCopy} className="h-7 px-2">
43
+ {copied ? <Check className="h-3.5 w-3.5 text-primary" /> : <Copy className="h-3.5 w-3.5" />}
44
+ </Button>
45
+ );
46
+ }
47
+
48
+ function StablecoinDeposit({
49
+ checkout,
50
+ onReset,
51
+ }: {
52
+ checkout: StablecoinCheckoutResult;
53
+ onReset: () => void;
54
+ }) {
55
+ const [confirmed, setConfirmed] = useState(false);
56
+ const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
57
+
58
+ useEffect(() => {
59
+ // TODO: poll charge status via tRPC query when backend endpoint exists
60
+ // For now, show the deposit address and let the user confirm manually
61
+ return () => {
62
+ if (pollRef.current) clearInterval(pollRef.current);
63
+ };
64
+ }, []);
65
+
66
+ const displayAmount = `${checkout.amountUsd} ${checkout.token}`;
67
+
68
+ if (confirmed) {
69
+ return (
70
+ <motion.div
71
+ initial={{ opacity: 0, scale: 0.95 }}
72
+ animate={{ opacity: 1, scale: 1 }}
73
+ className="rounded-md border border-primary/25 bg-primary/5 p-4 text-center"
74
+ >
75
+ <Check className="mx-auto h-8 w-8 text-primary" />
76
+ <p className="mt-2 text-sm font-medium">Payment detected. Credits will appear shortly.</p>
77
+ </motion.div>
78
+ );
79
+ }
80
+
81
+ return (
82
+ <motion.div initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} className="space-y-4">
83
+ <div className="rounded-md border p-4 space-y-3">
84
+ <div className="flex items-center justify-between">
85
+ <p className="text-sm text-muted-foreground">
86
+ Send exactly{" "}
87
+ <span className="font-mono font-bold text-foreground">{displayAmount}</span> to:
88
+ </p>
89
+ <Badge variant="outline" className="text-xs">
90
+ {checkout.token} on {checkout.chain}
91
+ </Badge>
92
+ </div>
93
+
94
+ <div className="flex items-center gap-2 rounded-md bg-muted/50 px-3 py-2">
95
+ <code className="flex-1 text-xs font-mono break-all text-foreground">
96
+ {checkout.depositAddress}
97
+ </code>
98
+ <CopyButton text={checkout.depositAddress} />
99
+ </div>
100
+
101
+ <p className="text-xs text-muted-foreground">
102
+ Only send {checkout.token} on the {checkout.chain} network. Other tokens or chains will be
103
+ lost.
104
+ </p>
105
+ </div>
106
+
107
+ <div className="flex gap-2">
108
+ <Button variant="ghost" size="sm" onClick={onReset}>
109
+ Cancel
110
+ </Button>
111
+ </div>
112
+ </motion.div>
113
+ );
114
+ }
115
+
19
116
  export function BuyCryptoCreditPanel() {
117
+ const [method, setMethod] = useState<PaymentMethod>("stablecoin");
20
118
  const [selected, setSelected] = useState<number | null>(null);
119
+ const [selectedToken, setSelectedToken] = useState(STABLECOIN_TOKENS[0]);
21
120
  const [loading, setLoading] = useState(false);
22
121
  const [error, setError] = useState<string | null>(null);
122
+ const [stablecoinCheckout, setStablecoinCheckout] = useState<StablecoinCheckoutResult | null>(
123
+ null,
124
+ );
23
125
 
24
- async function handleCheckout() {
126
+ async function handleBtcCheckout() {
25
127
  if (selected === null) return;
26
128
  setLoading(true);
27
129
  setError(null);
@@ -39,6 +141,30 @@ export function BuyCryptoCreditPanel() {
39
141
  }
40
142
  }
41
143
 
144
+ async function handleStablecoinCheckout() {
145
+ if (selected === null) return;
146
+ setLoading(true);
147
+ setError(null);
148
+ try {
149
+ const result = await createStablecoinCheckout(
150
+ selected,
151
+ selectedToken.token,
152
+ selectedToken.chain,
153
+ );
154
+ setStablecoinCheckout(result);
155
+ } catch {
156
+ setError("Checkout failed. Please try again.");
157
+ } finally {
158
+ setLoading(false);
159
+ }
160
+ }
161
+
162
+ function handleReset() {
163
+ setStablecoinCheckout(null);
164
+ setSelected(null);
165
+ setError(null);
166
+ }
167
+
42
168
  return (
43
169
  <motion.div
44
170
  initial={{ opacity: 0, y: 8 }}
@@ -48,47 +174,108 @@ export function BuyCryptoCreditPanel() {
48
174
  <Card>
49
175
  <CardHeader>
50
176
  <CardTitle className="flex items-center gap-2">
51
- <Bitcoin className="h-4 w-4 text-amber-500" />
177
+ {method === "btc" ? (
178
+ <Bitcoin className="h-4 w-4 text-amber-500" />
179
+ ) : (
180
+ <CircleDollarSign className="h-4 w-4 text-blue-500" />
181
+ )}
52
182
  Pay with Crypto
53
183
  </CardTitle>
54
- <p className="text-xs text-muted-foreground">
55
- Pay with BTC or other cryptocurrencies. Minimum $10.
56
- </p>
184
+ <div className="flex gap-2 pt-1">
185
+ <button
186
+ type="button"
187
+ onClick={() => {
188
+ setMethod("stablecoin");
189
+ handleReset();
190
+ }}
191
+ className={cn(
192
+ "rounded-full px-3 py-1 text-xs font-medium transition-colors",
193
+ method === "stablecoin"
194
+ ? "bg-primary/10 text-primary"
195
+ : "text-muted-foreground hover:text-foreground",
196
+ )}
197
+ >
198
+ Stablecoin
199
+ </button>
200
+ <button
201
+ type="button"
202
+ onClick={() => {
203
+ setMethod("btc");
204
+ handleReset();
205
+ }}
206
+ className={cn(
207
+ "rounded-full px-3 py-1 text-xs font-medium transition-colors",
208
+ method === "btc"
209
+ ? "bg-amber-500/10 text-amber-500"
210
+ : "text-muted-foreground hover:text-foreground",
211
+ )}
212
+ >
213
+ BTC
214
+ </button>
215
+ </div>
57
216
  </CardHeader>
58
217
  <CardContent className="space-y-4">
59
- <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
60
- {CRYPTO_AMOUNTS.map((amt) => (
61
- <motion.button
62
- key={amt.value}
63
- type="button"
64
- onClick={() => setSelected(amt.value)}
65
- whileHover={{ scale: 1.03 }}
66
- whileTap={{ scale: 0.98 }}
67
- transition={{
68
- type: "spring",
69
- stiffness: 400,
70
- damping: 25,
71
- }}
72
- className={cn(
73
- "flex flex-col items-center gap-1 rounded-md border p-3 text-sm font-medium transition-colors hover:bg-accent",
74
- selected === amt.value
75
- ? "border-primary bg-primary/5 ring-1 ring-primary shadow-[0_0_15px_rgba(0,255,65,0.3)]"
76
- : "border-border",
77
- )}
218
+ {stablecoinCheckout ? (
219
+ <StablecoinDeposit checkout={stablecoinCheckout} onReset={handleReset} />
220
+ ) : (
221
+ <>
222
+ {method === "stablecoin" && (
223
+ <div className="flex gap-2">
224
+ {STABLECOIN_TOKENS.map((t) => (
225
+ <button
226
+ key={`${t.token}:${t.chain}`}
227
+ type="button"
228
+ onClick={() => setSelectedToken(t)}
229
+ className={cn(
230
+ "rounded-md border px-3 py-1.5 text-xs font-medium transition-colors",
231
+ selectedToken.token === t.token && selectedToken.chain === t.chain
232
+ ? "border-primary bg-primary/5 text-primary"
233
+ : "border-border text-muted-foreground hover:text-foreground",
234
+ )}
235
+ >
236
+ {t.label}
237
+ </button>
238
+ ))}
239
+ </div>
240
+ )}
241
+
242
+ <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
243
+ {CRYPTO_AMOUNTS.map((amt) => (
244
+ <motion.button
245
+ key={amt.value}
246
+ type="button"
247
+ onClick={() => setSelected(amt.value)}
248
+ whileHover={{ scale: 1.03 }}
249
+ whileTap={{ scale: 0.98 }}
250
+ transition={{ type: "spring", stiffness: 400, damping: 25 }}
251
+ className={cn(
252
+ "flex flex-col items-center gap-1 rounded-md border p-3 text-sm font-medium transition-colors hover:bg-accent",
253
+ selected === amt.value
254
+ ? "border-primary bg-primary/5 ring-1 ring-primary shadow-[0_0_15px_rgba(0,255,65,0.3)]"
255
+ : "border-border",
256
+ )}
257
+ >
258
+ <span className="text-lg font-bold">{amt.label}</span>
259
+ </motion.button>
260
+ ))}
261
+ </div>
262
+
263
+ {error && <p className="text-sm text-destructive">{error}</p>}
264
+
265
+ <Button
266
+ onClick={method === "btc" ? handleBtcCheckout : handleStablecoinCheckout}
267
+ disabled={selected === null || loading}
268
+ variant="outline"
269
+ className="w-full sm:w-auto"
78
270
  >
79
- <span className="text-lg font-bold">{amt.label}</span>
80
- </motion.button>
81
- ))}
82
- </div>
83
- {error && <p className="text-sm text-destructive">{error}</p>}
84
- <Button
85
- onClick={handleCheckout}
86
- disabled={selected === null || loading}
87
- variant="outline"
88
- className="w-full sm:w-auto"
89
- >
90
- {loading ? "Opening checkout..." : "Pay with crypto"}
91
- </Button>
271
+ {loading
272
+ ? "Creating checkout..."
273
+ : method === "btc"
274
+ ? "Pay with BTC"
275
+ : `Pay with ${selectedToken.label}`}
276
+ </Button>
277
+ </>
278
+ )}
92
279
  </CardContent>
93
280
  </Card>
94
281
  </motion.div>
@@ -0,0 +1,32 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import type { Organization, OrgMember } from "@/lib/api";
5
+ import { useSession } from "@/lib/auth-client";
6
+
7
+ /**
8
+ * Derives the current user's role from an already-fetched org object.
9
+ * Does NOT fetch org data — avoids duplicate requests and role-flash.
10
+ * Pass the org from the page's own load() call.
11
+ */
12
+ export function useMyOrgRole(org: Organization | null): OrgMember["role"] | null {
13
+ const { data: session } = useSession();
14
+
15
+ return useMemo(() => {
16
+ if (!org || !session?.user?.id) return null;
17
+
18
+ const me = org.members?.find(
19
+ (m: OrgMember) => m.userId === session.user.id || m.email === session.user.email,
20
+ );
21
+ return me?.role ?? null;
22
+ }, [org, session?.user?.id, session?.user?.email]);
23
+ }
24
+
25
+ /**
26
+ * Returns true if the current user is an admin or owner of the given org.
27
+ * Pass the org from the page's own load() call.
28
+ */
29
+ export function useIsAdminOrOwner(org: Organization | null): boolean {
30
+ const role = useMyOrgRole(org);
31
+ return role === "admin" || role === "owner";
32
+ }
package/src/lib/api.ts CHANGED
@@ -1336,6 +1336,23 @@ export async function createCryptoCheckout(
1336
1336
  return trpcVanilla.billing.cryptoCheckout.mutate({ amountUsd });
1337
1337
  }
1338
1338
 
1339
+ export interface StablecoinCheckoutResult {
1340
+ depositAddress: string;
1341
+ amountRaw: string;
1342
+ amountUsd: number;
1343
+ chain: string;
1344
+ token: string;
1345
+ referenceId: string;
1346
+ }
1347
+
1348
+ export async function createStablecoinCheckout(
1349
+ amountUsd: number,
1350
+ token: string,
1351
+ chain: string,
1352
+ ): Promise<StablecoinCheckoutResult> {
1353
+ return trpcVanilla.billing.stablecoinCheckout.mutate({ amountUsd, token, chain });
1354
+ }
1355
+
1339
1356
  // --- Dividend types ---
1340
1357
 
1341
1358
  export interface DividendWalletStats {
@@ -72,3 +72,13 @@ export async function createOrganization(data: {
72
72
  }): Promise<{ id: string; name: string; slug: string }> {
73
73
  return trpcVanilla.org.createOrganization.mutate(data);
74
74
  }
75
+
76
+ export async function listMyOrganizations(): Promise<
77
+ Array<{ id: string; name: string; slug: string; role: "owner" | "admin" | "member" }>
78
+ > {
79
+ return trpcVanilla.org.listMyOrganizations.query(undefined);
80
+ }
81
+
82
+ export async function acceptInvite(token: string): Promise<{ orgId: string; orgName: string }> {
83
+ return trpcVanilla.org.acceptInvite.mutate({ token });
84
+ }
@@ -113,6 +113,7 @@ type AppRouterRecord = {
113
113
  affiliateStats: AnyTRPCQueryProcedure;
114
114
  affiliateReferrals: AnyTRPCQueryProcedure;
115
115
  cryptoCheckout: AnyTRPCMutationProcedure;
116
+ stablecoinCheckout: AnyTRPCMutationProcedure;
116
117
  autoTopupSettings: AnyTRPCQueryProcedure;
117
118
  updateAutoTopupSettings: AnyTRPCMutationProcedure;
118
119
  accountStatus: AnyTRPCQueryProcedure;
@@ -157,6 +158,7 @@ type AppRouterRecord = {
157
158
  transferOwnership: AnyTRPCMutationProcedure;
158
159
  deleteOrganization: AnyTRPCMutationProcedure;
159
160
  listMyOrganizations: AnyTRPCQueryProcedure;
161
+ acceptInvite: AnyTRPCMutationProcedure;
160
162
  orgBillingBalance: AnyTRPCQueryProcedure;
161
163
  orgMemberUsage: AnyTRPCQueryProcedure;
162
164
  orgBillingInfo: AnyTRPCQueryProcedure;