@aglyn/plugins-marketplace 1.2.0 → 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.
Files changed (73) hide show
  1. package/README.md +1 -1
  2. package/package.json +14 -14
  3. package/src/lib/components/listing-content.component.js +11 -3
  4. package/src/lib/components/listing-content.component.js.map +1 -1
  5. package/src/lib/components/marketplace-pay-panel.component.d.ts +67 -0
  6. package/src/lib/components/marketplace-pay-panel.component.js +228 -0
  7. package/src/lib/components/marketplace-pay-panel.component.js.map +1 -0
  8. package/src/lib/components/publish-artifact-dialog.component.d.ts +1 -1
  9. package/src/lib/components/publish-artifact-dialog.component.js +144 -20
  10. package/src/lib/components/publish-artifact-dialog.component.js.map +1 -1
  11. package/src/lib/components/publish-starter-fields.component.d.ts +35 -0
  12. package/src/lib/components/publish-starter-fields.component.js +176 -0
  13. package/src/lib/components/publish-starter-fields.component.js.map +1 -0
  14. package/src/lib/components/starter-picker-shelf.component.d.ts +2 -2
  15. package/src/lib/components/starter-picker-shelf.component.js +28 -6
  16. package/src/lib/components/starter-picker-shelf.component.js.map +1 -1
  17. package/src/lib/hooks/use-marketplace-actions.d.ts +9 -3
  18. package/src/lib/hooks/use-marketplace-actions.js +47 -89
  19. package/src/lib/hooks/use-marketplace-actions.js.map +1 -1
  20. package/src/lib/hooks/use-marketplace-pay.d.ts +70 -0
  21. package/src/lib/hooks/use-marketplace-pay.js +359 -0
  22. package/src/lib/hooks/use-marketplace-pay.js.map +1 -0
  23. package/src/lib/hooks/use-starter-publish-facts.d.ts +25 -0
  24. package/src/lib/hooks/use-starter-publish-facts.js +116 -0
  25. package/src/lib/hooks/use-starter-publish-facts.js.map +1 -0
  26. package/src/lib/hooks/use-starter-purchase.d.ts +17 -22
  27. package/src/lib/hooks/use-starter-purchase.js +26 -73
  28. package/src/lib/hooks/use-starter-purchase.js.map +1 -1
  29. package/src/lib/model/marketplace-invoice.d.ts +52 -0
  30. package/src/lib/model/marketplace-invoice.js +75 -0
  31. package/src/lib/model/marketplace-invoice.js.map +1 -0
  32. package/src/lib/model/marketplace-pay.d.ts +42 -0
  33. package/src/lib/model/marketplace-pay.js +33 -0
  34. package/src/lib/model/marketplace-pay.js.map +1 -0
  35. package/src/lib/model/platform-seller.d.ts +37 -0
  36. package/src/lib/model/platform-seller.js +36 -0
  37. package/src/lib/model/platform-seller.js.map +1 -0
  38. package/src/lib/model/starter-publish.d.ts +159 -0
  39. package/src/lib/model/starter-publish.js +189 -0
  40. package/src/lib/model/starter-publish.js.map +1 -0
  41. package/src/lib/model/starter-query.d.ts +0 -8
  42. package/src/lib/model/starter-query.js +0 -10
  43. package/src/lib/model/starter-query.js.map +1 -1
  44. package/src/lib/server/billing-webhook.d.ts +6 -0
  45. package/src/lib/server/billing-webhook.js +269 -211
  46. package/src/lib/server/billing-webhook.js.map +1 -1
  47. package/src/lib/server/checkout.d.ts +2 -48
  48. package/src/lib/server/checkout.js +21 -241
  49. package/src/lib/server/checkout.js.map +1 -1
  50. package/src/lib/server/pay.d.ts +18 -0
  51. package/src/lib/server/pay.js +540 -0
  52. package/src/lib/server/pay.js.map +1 -0
  53. package/src/lib/server/platform-org.d.ts +23 -0
  54. package/src/lib/server/platform-org.js +27 -0
  55. package/src/lib/server/platform-org.js.map +1 -0
  56. package/src/lib/server/publish-preconditions.js +4 -1
  57. package/src/lib/server/publish-preconditions.js.map +1 -1
  58. package/src/lib/server/publish-starter.js +5 -2
  59. package/src/lib/server/publish-starter.js.map +1 -1
  60. package/src/lib/server/publisher-profile.d.ts +5 -0
  61. package/src/lib/server/publisher-profile.js +20 -1
  62. package/src/lib/server/publisher-profile.js.map +1 -1
  63. package/src/lib/server/publisher-standing.d.ts +26 -0
  64. package/src/lib/server/publisher-standing.js +66 -0
  65. package/src/lib/server/publisher-standing.js.map +1 -0
  66. package/src/lib/server/sale.d.ts +119 -0
  67. package/src/lib/server/sale.js +304 -0
  68. package/src/lib/server/sale.js.map +1 -0
  69. package/src/lib/server/starter-publish-facts.d.ts +36 -0
  70. package/src/lib/server/starter-publish-facts.js +135 -0
  71. package/src/lib/server/starter-publish-facts.js.map +1 -0
  72. package/src/lib/server.js +8 -0
  73. package/src/lib/server.js.map +1 -1
@@ -0,0 +1,359 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx } from "react/jsx-runtime";
19
+ import { lockdownRefusalText, parseLockdownRefusal } from "@aglyn/aglyn/app-utils/lockdown";
20
+ import { readGaClientId } from "@aglyn/aglyn/app-utils/analytics-events";
21
+ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
22
+ import { handleStripeNextAction } from "@aglyn/shared-ui-jsx/stripe/lazy-stripe";
23
+ import { useUser } from "@aglyn/tenant-feature-instance";
24
+ import { useCallback, useRef, useState } from "react";
25
+ import { MarketplacePayPanel } from "../components/marketplace-pay-panel.component.js";
26
+ import { payPrerequisiteOf } from "../model/marketplace-pay.js";
27
+ const newAttemptKey = ()=>`${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
28
+ /**
29
+ * Buying a marketplace listing in the console (AGL-4049): the card on file, or
30
+ * a card added right there, then the bank's step if the bank asks for one.
31
+ *
32
+ * Asks the door what it would charge before it shows anything, so the total,
33
+ * the prerequisite or the refusal is the first thing the buyer sees. Stripe
34
+ * loads only if a form or the bank's step actually needs it.
35
+ *
36
+ * One attempt per press of Pay: the same key goes with a double click and with
37
+ * a retry after a lost connection, so neither can charge twice (AGL-1697). An
38
+ * answer that ends the attempt retires the key, and pressing Pay again after a
39
+ * decline is a new attempt.
40
+ *
41
+ * @param input.billingHref the workspace's Billing page, for a prerequisite
42
+ * this person cannot meet themselves.
43
+ */ export function useMarketplacePay(input) {
44
+ const { orgId, hostId, billingHref } = input;
45
+ const { data: user } = useUser();
46
+ const userRef = useRef(user);
47
+ userRef.current = user;
48
+ const attemptKeys = useRef(new Map());
49
+ const [listing, setListing] = useState(null);
50
+ const [step, setStep] = useState(null);
51
+ const [busy, setBusy] = useState(false);
52
+ const [error, setError] = useState(null);
53
+ /** Hands the outcome to the `buy` that is waiting on it. */ const settled = useRef(null);
54
+ const finish = useCallback((result)=>{
55
+ setStep(null);
56
+ setListing(null);
57
+ setBusy(false);
58
+ setError(null);
59
+ settled.current == null ? void 0 : settled.current.call(settled, result);
60
+ settled.current = null;
61
+ }, []);
62
+ const askDoor = useCallback(async (target, body, attemptKey)=>{
63
+ const response = await authorizedFetch(userRef.current, '/api/marketplace/pay', {
64
+ method: 'POST',
65
+ headers: _extends({
66
+ 'Content-Type': 'application/json'
67
+ }, attemptKey ? {
68
+ 'Idempotency-Key': attemptKey
69
+ } : {}),
70
+ body: JSON.stringify(_extends({
71
+ listingId: target.listingId,
72
+ orgId
73
+ }, hostId ? {
74
+ hostId
75
+ } : {}, body))
76
+ });
77
+ const payload = await response.json().catch(()=>null);
78
+ return {
79
+ ok: response.ok,
80
+ status: response.status,
81
+ payload
82
+ };
83
+ }, [
84
+ orgId,
85
+ hostId
86
+ ]);
87
+ /** What a refused answer says to the buyer. */ const refusalText = (answer)=>{
88
+ var _answer_payload;
89
+ const locked = parseLockdownRefusal(answer.status, answer.payload);
90
+ if (locked) return lockdownRefusalText(locked);
91
+ if (answer.status === 501) return 'Purchases are not set up on this deployment.';
92
+ const stated = (_answer_payload = answer.payload) == null ? void 0 : _answer_payload['error'];
93
+ return typeof stated === 'string' && stated ? stated : 'The payment did not go through.';
94
+ };
95
+ const billingRequest = useCallback(async (body)=>{
96
+ const response = await authorizedFetch(userRef.current, '/api/billing/profile', {
97
+ method: 'POST',
98
+ headers: {
99
+ 'Content-Type': 'application/json'
100
+ },
101
+ body: JSON.stringify(_extends({
102
+ orgId
103
+ }, body))
104
+ });
105
+ const payload = await response.json().catch(()=>null);
106
+ if (!response.ok) {
107
+ throw new Error(typeof (payload == null ? void 0 : payload.error) === 'string' ? payload.error : 'Billing could not be reached.');
108
+ }
109
+ return payload;
110
+ }, [
111
+ orgId
112
+ ]);
113
+ /** Moves to the step a prerequisite needs: a form here, or who can fix it. */ const meetPrerequisite = useCallback(async (need, answer)=>{
114
+ var _answer_payload;
115
+ if (((_answer_payload = answer.payload) == null ? void 0 : _answer_payload['canManageBilling']) !== true) {
116
+ var _answer_payload1;
117
+ const amountCents = (_answer_payload1 = answer.payload) == null ? void 0 : _answer_payload1['amountCents'];
118
+ return setStep({
119
+ kind: 'ask-billing',
120
+ need,
121
+ amountCents: typeof amountCents === 'number' ? amountCents : null
122
+ });
123
+ }
124
+ if (need === 'billing_address_required') return setStep({
125
+ kind: 'address'
126
+ });
127
+ setStep({
128
+ kind: 'card',
129
+ clientSecret: null
130
+ });
131
+ try {
132
+ const opened = await billingRequest({
133
+ action: 'create-setup-intent'
134
+ });
135
+ const secret = opened == null ? void 0 : opened['clientSecret'];
136
+ if (typeof secret !== 'string') throw new Error('The card form could not open.');
137
+ setStep({
138
+ kind: 'card',
139
+ clientSecret: secret
140
+ });
141
+ } catch (caught) {
142
+ setError(caught.message);
143
+ }
144
+ }, [
145
+ billingRequest
146
+ ]);
147
+ /**
148
+ * Asks what it would cost now, and shows whatever that answer needs: the
149
+ * result when the purchase ends here, `null` when a step is on screen.
150
+ */ const quote = useCallback(async (target)=>{
151
+ var _answer_payload, _answer_payload1;
152
+ const answer = await askDoor(target, {
153
+ action: 'quote'
154
+ });
155
+ if (answer.ok && ((_answer_payload = answer.payload) == null ? void 0 : _answer_payload['quote'])) {
156
+ setStep({
157
+ kind: 'confirm',
158
+ quote: answer.payload['quote']
159
+ });
160
+ return null;
161
+ }
162
+ if (answer.status === 409 && ((_answer_payload1 = answer.payload) == null ? void 0 : _answer_payload1['code']) === 'already_purchased') {
163
+ return {
164
+ outcome: 'owned',
165
+ error: null
166
+ };
167
+ }
168
+ const need = payPrerequisiteOf(answer.status, answer.payload);
169
+ if (need) {
170
+ await meetPrerequisite(need, answer);
171
+ return null;
172
+ }
173
+ return {
174
+ outcome: 'stopped',
175
+ error: refusalText(answer),
176
+ locked: Boolean(parseLockdownRefusal(answer.status, answer.payload))
177
+ };
178
+ }, [
179
+ askDoor,
180
+ meetPrerequisite
181
+ ]);
182
+ const buy = useCallback((target)=>{
183
+ if (!orgId || !target.listingId) {
184
+ return Promise.resolve({
185
+ outcome: 'stopped',
186
+ error: null
187
+ });
188
+ }
189
+ return new Promise((resolve)=>{
190
+ settled.current = resolve;
191
+ setListing(target);
192
+ setError(null);
193
+ setBusy(true);
194
+ quote(target).then((ended)=>{
195
+ if (ended) finish(ended);
196
+ else setBusy(false);
197
+ }).catch((caught)=>{
198
+ console.error(caught);
199
+ finish({
200
+ outcome: 'stopped',
201
+ error: 'The payment could not open. Check your connection and try again.'
202
+ });
203
+ });
204
+ });
205
+ }, [
206
+ orgId,
207
+ quote,
208
+ finish
209
+ ]);
210
+ /** Goes back to the total once a card or an address is on file. */ const returnToQuote = useCallback(async ()=>{
211
+ if (!listing) return;
212
+ setBusy(true);
213
+ setError(null);
214
+ try {
215
+ const ended = await quote(listing);
216
+ if ((ended == null ? void 0 : ended.outcome) === 'owned') return finish(ended);
217
+ if (ended) setError(ended.error);
218
+ } finally{
219
+ setBusy(false);
220
+ }
221
+ }, [
222
+ listing,
223
+ quote,
224
+ finish
225
+ ]);
226
+ const onCardSaved = useCallback(async (setupIntentId)=>{
227
+ setError(null);
228
+ try {
229
+ await billingRequest({
230
+ action: 'finalize-card-setup',
231
+ setupIntentId
232
+ });
233
+ } catch (caught) {
234
+ return setError(caught.message);
235
+ }
236
+ await returnToQuote();
237
+ }, [
238
+ billingRequest,
239
+ returnToQuote
240
+ ]);
241
+ const onAddressSaved = useCallback(async (address)=>{
242
+ setError(null);
243
+ try {
244
+ await billingRequest(_extends({
245
+ action: 'set-billing-address'
246
+ }, address));
247
+ } catch (caught) {
248
+ return setError(caught.message);
249
+ }
250
+ await returnToQuote();
251
+ }, [
252
+ billingRequest,
253
+ returnToQuote
254
+ ]);
255
+ const onPay = useCallback(async ()=>{
256
+ var _attemptKeys_current_get;
257
+ if (!listing || (step == null ? void 0 : step.kind) !== 'confirm') return;
258
+ const { listingId } = listing;
259
+ const key = (_attemptKeys_current_get = attemptKeys.current.get(listingId)) != null ? _attemptKeys_current_get : newAttemptKey();
260
+ attemptKeys.current.set(listingId, key);
261
+ setBusy(true);
262
+ setError(null);
263
+ try {
264
+ var _answer_payload, _answer_payload1;
265
+ const answer = await askDoor(listing, {
266
+ expectedTotalCents: step.quote.totalCents,
267
+ gaClientId: await readGaClientId(process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID)
268
+ }, key);
269
+ if (answer.ok) {
270
+ var _answer_payload2;
271
+ const { paid, submitted, requiresAction, paymentClientSecret, invoiceId } = (_answer_payload2 = answer.payload) != null ? _answer_payload2 : {};
272
+ attemptKeys.current.delete(listingId);
273
+ if (paid) return finish({
274
+ outcome: 'paid',
275
+ error: null
276
+ });
277
+ if (submitted) return finish({
278
+ outcome: 'pending',
279
+ error: null
280
+ });
281
+ if (requiresAction && typeof paymentClientSecret === 'string') {
282
+ var _settledAnswer_payload, _settledAnswer_payload1;
283
+ const refused = await handleStripeNextAction(paymentClientSecret);
284
+ // The server decides, either way: it reads the invoice, records a
285
+ // payment that landed, and voids one that did not.
286
+ const settledAnswer = await askDoor(listing, {
287
+ action: 'settle',
288
+ invoiceId
289
+ });
290
+ if (settledAnswer.ok && ((_settledAnswer_payload = settledAnswer.payload) == null ? void 0 : _settledAnswer_payload['paid'])) {
291
+ return finish({
292
+ outcome: 'paid',
293
+ error: null
294
+ });
295
+ }
296
+ if (settledAnswer.ok && ((_settledAnswer_payload1 = settledAnswer.payload) == null ? void 0 : _settledAnswer_payload1['submitted'])) {
297
+ return finish({
298
+ outcome: 'pending',
299
+ error: null
300
+ });
301
+ }
302
+ return setError(refused != null ? refused : refusalText(settledAnswer));
303
+ }
304
+ return setError('The payment did not go through.');
305
+ }
306
+ // An answer ends this attempt: the next press of Pay is a new one.
307
+ attemptKeys.current.delete(listingId);
308
+ if (answer.status === 409 && ((_answer_payload = answer.payload) == null ? void 0 : _answer_payload['code']) === 'already_purchased') {
309
+ return finish({
310
+ outcome: 'owned',
311
+ error: null
312
+ });
313
+ }
314
+ if (answer.status === 409 && ((_answer_payload1 = answer.payload) == null ? void 0 : _answer_payload1['code']) === 'total_changed') {
315
+ await returnToQuote();
316
+ return setError(refusalText(answer));
317
+ }
318
+ const need = payPrerequisiteOf(answer.status, answer.payload);
319
+ if (need) return await meetPrerequisite(need, answer);
320
+ setError(refusalText(answer));
321
+ } catch (caught) {
322
+ // No answer came back, so this attempt may still be running: the key
323
+ // stays, and a second press replays it rather than charging again.
324
+ console.error(caught);
325
+ setError('The payment could not be confirmed. Check your connection and try again.');
326
+ } finally{
327
+ setBusy(false);
328
+ }
329
+ }, [
330
+ listing,
331
+ step,
332
+ askDoor,
333
+ finish,
334
+ returnToQuote,
335
+ meetPrerequisite
336
+ ]);
337
+ const panel = step && listing ? /*#__PURE__*/ _jsx(MarketplacePayPanel, {
338
+ step: step,
339
+ listingName: listing.displayName || 'this listing',
340
+ busy: busy,
341
+ error: error,
342
+ billingHref: billingHref,
343
+ onPay: ()=>void onPay(),
344
+ onCardSaved: onCardSaved,
345
+ onAddressSaved: onAddressSaved,
346
+ onCancel: ()=>finish({
347
+ outcome: 'stopped',
348
+ error: null
349
+ })
350
+ }) : null;
351
+ return {
352
+ buy,
353
+ busy,
354
+ panel
355
+ };
356
+ }
357
+ export default useMarketplacePay;
358
+
359
+ //# sourceMappingURL=use-marketplace-pay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/hooks/use-marketplace-pay.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn/app-utils/lockdown'\nimport { readGaClientId } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport type { BillingAddressValue } from '@aglyn/shared-ui-jsx/stripe/billing-address-form.component'\nimport { handleStripeNextAction } from '@aglyn/shared-ui-jsx/stripe/lazy-stripe'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { useCallback, useRef, useState, type ReactNode } from 'react'\nimport { MarketplacePayPanel, type PayStep } from '../components/marketplace-pay-panel.component'\nimport {\n payPrerequisiteOf,\n type PayPrerequisite,\n type PayQuote,\n} from '../model/marketplace-pay'\n\n/** How one purchase ended. */\nexport type MarketplacePayOutcome =\n /** Paid, and the purchase is on record: the buyer owns it now. */\n | 'paid'\n /** The workspace owns it already: nothing was charged. */\n | 'owned'\n /** Stripe accepted the payment and it has not cleared yet. */\n | 'pending'\n /** Closed before paying, or it could not open: `error` says which. */\n | 'stopped'\n\n/** How one purchase ended, and why it could not open, if it could not. */\nexport interface MarketplacePayResult {\n outcome: MarketplacePayOutcome\n error: string | null\n /** The refusal is a feature lockdown: a pause to read, not a failure of theirs. */\n locked?: boolean\n}\n\n/** The listing being bought, as a surface knows it. */\nexport interface PayListing {\n listingId: string\n displayName?: string | null\n}\n\nexport interface MarketplacePay {\n /** Opens payment for a listing. Settles when the purchase has ended, either way. */\n buy: (listing: PayListing) => Promise<MarketplacePayResult>\n /** Asking what it would cost, or paying. */\n busy: boolean\n /** The panel, in place under the button that asked for it, or `null`. */\n panel: ReactNode\n}\n\ninterface DoorAnswer {\n ok: boolean\n status: number\n payload: Record<string, any> | null\n}\n\nconst newAttemptKey = () =>\n `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n\n/**\n * Buying a marketplace listing in the console (AGL-4049): the card on file, or\n * a card added right there, then the bank's step if the bank asks for one.\n *\n * Asks the door what it would charge before it shows anything, so the total,\n * the prerequisite or the refusal is the first thing the buyer sees. Stripe\n * loads only if a form or the bank's step actually needs it.\n *\n * One attempt per press of Pay: the same key goes with a double click and with\n * a retry after a lost connection, so neither can charge twice (AGL-1697). An\n * answer that ends the attempt retires the key, and pressing Pay again after a\n * decline is a new attempt.\n *\n * @param input.billingHref the workspace's Billing page, for a prerequisite\n * this person cannot meet themselves.\n */\nexport function useMarketplacePay(input: {\n orgId: string | null | undefined\n /** Named so the dispatcher can gate the site this is bought from, when there is one. */\n hostId?: string | null\n billingHref?: string | null\n}): MarketplacePay {\n const { orgId, hostId, billingHref } = input\n const { data: user } = useUser()\n const userRef = useRef(user)\n userRef.current = user\n const attemptKeys = useRef(new Map<string, string>())\n const [listing, setListing] = useState<PayListing | null>(null)\n const [step, setStep] = useState<PayStep | null>(null)\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n /** Hands the outcome to the `buy` that is waiting on it. */\n const settled = useRef<((result: MarketplacePayResult) => void) | null>(null)\n\n const finish = useCallback((result: MarketplacePayResult) => {\n setStep(null)\n setListing(null)\n setBusy(false)\n setError(null)\n settled.current?.(result)\n settled.current = null\n }, [])\n\n const askDoor = useCallback(\n async (\n target: PayListing,\n body: Record<string, unknown>,\n attemptKey?: string,\n ): Promise<DoorAnswer> => {\n const response = await authorizedFetch(userRef.current, '/api/marketplace/pay', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(attemptKey ? { 'Idempotency-Key': attemptKey } : {}),\n },\n body: JSON.stringify({\n listingId: target.listingId,\n orgId,\n ...(hostId ? { hostId } : {}),\n ...body,\n }),\n })\n const payload = (await response.json().catch((): null => null)) as DoorAnswer['payload']\n return { ok: response.ok, status: response.status, payload }\n },\n [orgId, hostId],\n )\n\n /** What a refused answer says to the buyer. */\n const refusalText = (answer: DoorAnswer): string => {\n const locked = parseLockdownRefusal(answer.status, answer.payload)\n if (locked) return lockdownRefusalText(locked)\n if (answer.status === 501) return 'Purchases are not set up on this deployment.'\n const stated = answer.payload?.['error']\n return typeof stated === 'string' && stated ? stated : 'The payment did not go through.'\n }\n\n const billingRequest = useCallback(\n async (body: Record<string, unknown>): Promise<Record<string, any> | null> => {\n const response = await authorizedFetch(userRef.current, '/api/billing/profile', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ orgId, ...body }),\n })\n const payload = await response.json().catch((): null => null)\n if (!response.ok) {\n throw new Error(\n typeof payload?.error === 'string' ? payload.error : 'Billing could not be reached.',\n )\n }\n return payload\n },\n [orgId],\n )\n\n /** Moves to the step a prerequisite needs: a form here, or who can fix it. */\n const meetPrerequisite = useCallback(\n async (need: PayPrerequisite, answer: DoorAnswer) => {\n if (answer.payload?.['canManageBilling'] !== true) {\n const amountCents = answer.payload?.['amountCents']\n return setStep({\n kind: 'ask-billing',\n need,\n amountCents: typeof amountCents === 'number' ? amountCents : null,\n })\n }\n if (need === 'billing_address_required') return setStep({ kind: 'address' })\n setStep({ kind: 'card', clientSecret: null })\n try {\n const opened = await billingRequest({ action: 'create-setup-intent' })\n const secret = opened?.['clientSecret']\n if (typeof secret !== 'string') throw new Error('The card form could not open.')\n setStep({ kind: 'card', clientSecret: secret })\n } catch (caught) {\n setError((caught as Error).message)\n }\n },\n [billingRequest],\n )\n\n /**\n * Asks what it would cost now, and shows whatever that answer needs: the\n * result when the purchase ends here, `null` when a step is on screen.\n */\n const quote = useCallback(\n async (target: PayListing): Promise<MarketplacePayResult | null> => {\n const answer = await askDoor(target, { action: 'quote' })\n if (answer.ok && answer.payload?.['quote']) {\n setStep({ kind: 'confirm', quote: answer.payload['quote'] as PayQuote })\n return null\n }\n if (answer.status === 409 && answer.payload?.['code'] === 'already_purchased') {\n return { outcome: 'owned', error: null }\n }\n const need = payPrerequisiteOf(answer.status, answer.payload)\n if (need) {\n await meetPrerequisite(need, answer)\n return null\n }\n return {\n outcome: 'stopped',\n error: refusalText(answer),\n locked: Boolean(parseLockdownRefusal(answer.status, answer.payload)),\n }\n },\n [askDoor, meetPrerequisite],\n )\n\n const buy = useCallback(\n (target: PayListing): Promise<MarketplacePayResult> => {\n if (!orgId || !target.listingId) {\n return Promise.resolve({ outcome: 'stopped', error: null })\n }\n return new Promise<MarketplacePayResult>((resolve) => {\n settled.current = resolve\n setListing(target)\n setError(null)\n setBusy(true)\n quote(target)\n .then((ended) => {\n if (ended) finish(ended)\n else setBusy(false)\n })\n .catch((caught: unknown) => {\n console.error(caught)\n finish({\n outcome: 'stopped',\n error: 'The payment could not open. Check your connection and try again.',\n })\n })\n })\n },\n [orgId, quote, finish],\n )\n\n /** Goes back to the total once a card or an address is on file. */\n const returnToQuote = useCallback(async () => {\n if (!listing) return\n setBusy(true)\n setError(null)\n try {\n const ended = await quote(listing)\n if (ended?.outcome === 'owned') return finish(ended)\n if (ended) setError(ended.error)\n } finally {\n setBusy(false)\n }\n }, [listing, quote, finish])\n\n const onCardSaved = useCallback(\n async (setupIntentId: string) => {\n setError(null)\n try {\n await billingRequest({ action: 'finalize-card-setup', setupIntentId })\n } catch (caught) {\n return setError((caught as Error).message)\n }\n await returnToQuote()\n },\n [billingRequest, returnToQuote],\n )\n\n const onAddressSaved = useCallback(\n async (address: BillingAddressValue) => {\n setError(null)\n try {\n await billingRequest({ action: 'set-billing-address', ...address })\n } catch (caught) {\n return setError((caught as Error).message)\n }\n await returnToQuote()\n },\n [billingRequest, returnToQuote],\n )\n\n const onPay = useCallback(async () => {\n if (!listing || step?.kind !== 'confirm') return\n const { listingId } = listing\n const key = attemptKeys.current.get(listingId) ?? newAttemptKey()\n attemptKeys.current.set(listingId, key)\n setBusy(true)\n setError(null)\n try {\n const answer = await askDoor(\n listing,\n {\n expectedTotalCents: step.quote.totalCents,\n gaClientId: await readGaClientId(process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID),\n },\n key,\n )\n if (answer.ok) {\n const { paid, submitted, requiresAction, paymentClientSecret, invoiceId } =\n answer.payload ?? {}\n attemptKeys.current.delete(listingId)\n if (paid) return finish({ outcome: 'paid', error: null })\n if (submitted) return finish({ outcome: 'pending', error: null })\n if (requiresAction && typeof paymentClientSecret === 'string') {\n const refused = await handleStripeNextAction(paymentClientSecret)\n // The server decides, either way: it reads the invoice, records a\n // payment that landed, and voids one that did not.\n const settledAnswer = await askDoor(listing, { action: 'settle', invoiceId })\n if (settledAnswer.ok && settledAnswer.payload?.['paid']) {\n return finish({ outcome: 'paid', error: null })\n }\n if (settledAnswer.ok && settledAnswer.payload?.['submitted']) {\n return finish({ outcome: 'pending', error: null })\n }\n return setError(refused ?? refusalText(settledAnswer))\n }\n return setError('The payment did not go through.')\n }\n // An answer ends this attempt: the next press of Pay is a new one.\n attemptKeys.current.delete(listingId)\n if (answer.status === 409 && answer.payload?.['code'] === 'already_purchased') {\n return finish({ outcome: 'owned', error: null })\n }\n if (answer.status === 409 && answer.payload?.['code'] === 'total_changed') {\n await returnToQuote()\n return setError(refusalText(answer))\n }\n const need = payPrerequisiteOf(answer.status, answer.payload)\n if (need) return await meetPrerequisite(need, answer)\n setError(refusalText(answer))\n } catch (caught) {\n // No answer came back, so this attempt may still be running: the key\n // stays, and a second press replays it rather than charging again.\n console.error(caught)\n setError('The payment could not be confirmed. Check your connection and try again.')\n } finally {\n setBusy(false)\n }\n }, [listing, step, askDoor, finish, returnToQuote, meetPrerequisite])\n\n const panel =\n step && listing ? (\n <MarketplacePayPanel\n step={step}\n listingName={listing.displayName || 'this listing'}\n busy={busy}\n error={error}\n billingHref={billingHref}\n onPay={() => void onPay()}\n onCardSaved={onCardSaved}\n onAddressSaved={onAddressSaved}\n onCancel={() => finish({ outcome: 'stopped', error: null })}\n />\n ) : null\n\n return { buy, busy, panel }\n}\n\nexport default useMarketplacePay\n"],"names":["lockdownRefusalText","parseLockdownRefusal","readGaClientId","authorizedFetch","handleStripeNextAction","useUser","useCallback","useRef","useState","MarketplacePayPanel","payPrerequisiteOf","newAttemptKey","Date","now","toString","Math","random","slice","useMarketplacePay","input","orgId","hostId","billingHref","data","user","userRef","current","attemptKeys","Map","listing","setListing","step","setStep","busy","setBusy","error","setError","settled","finish","result","askDoor","target","body","attemptKey","response","method","headers","JSON","stringify","listingId","payload","json","catch","ok","status","refusalText","answer","locked","stated","billingRequest","Error","meetPrerequisite","need","amountCents","kind","clientSecret","opened","action","secret","caught","message","quote","outcome","Boolean","buy","Promise","resolve","then","ended","console","returnToQuote","onCardSaved","setupIntentId","onAddressSaved","address","onPay","key","get","set","expectedTotalCents","totalCents","gaClientId","process","env","NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID","paid","submitted","requiresAction","paymentClientSecret","invoiceId","delete","settledAnswer","refused","panel","listingName","displayName","onCancel"],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import { type SiteStarterBuiltinReading } from '@aglyn/aglyn/app-utils/site-starter-theme';
2
+ import { type StarterPublishFacts } from '../model/starter-publish';
3
+ /** Everything "Publish as a starter" reads before the click. */
4
+ export interface StarterPublishFactsState {
5
+ /** The reads; the form adds the price it holds. */
6
+ facts: Omit<StarterPublishFacts, 'priceUsd'>;
7
+ /** The site's built-in theme as a starter would carry it, or `null` when it wears none. */
8
+ builtin: SiteStarterBuiltinReading | null;
9
+ /** The host document has arrived, so `builtin` is an answer rather than a wait. */
10
+ hostReady: boolean;
11
+ }
12
+ /**
13
+ * What a starter publish of one site would capture and what stands in its
14
+ * way (AGL-4049): the host (its theme and routing), the routed pages it
15
+ * would take, the publishing org's profile, and its plan.
16
+ *
17
+ * The routed pages are read by id, at most one past the most a starter may
18
+ * have, the same window the door walks: enough to count them and to say a
19
+ * site is past the limit, never the whole screens collection.
20
+ */
21
+ export declare function useStarterPublishFacts(input: {
22
+ hostId: string | null | undefined;
23
+ orgId: string | null | undefined;
24
+ }): StarterPublishFactsState;
25
+ export default useStarterPublishFacts;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { buildRoute, Route } from "@aglyn/aglyn/app-utils/console-routes";
18
+ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
19
+ import { SITE_STARTER_MAX_SCREENS } from "@aglyn/aglyn/app-utils/site-starter";
20
+ import { siteStarterBuiltinThemeOf } from "@aglyn/aglyn/app-utils/site-starter-theme";
21
+ import { useConsoleHostRoute, useFirestore, useFirestoreCollection, useFirestoreDoc, usePlanFeature, useUser } from "@aglyn/tenant-feature-instance";
22
+ import { collection, doc, documentId, query, where } from "firebase/firestore";
23
+ import { useEffect, useMemo, useState } from "react";
24
+ import { orgMarketplacePath } from "../model/marketplace-paths.js";
25
+ import { payoutsReady, starterPublishPages } from "../model/starter-publish.js";
26
+ /**
27
+ * What a starter publish of one site would capture and what stands in its
28
+ * way (AGL-4049): the host (its theme and routing), the routed pages it
29
+ * would take, the publishing org's profile, and its plan.
30
+ *
31
+ * The routed pages are read by id, at most one past the most a starter may
32
+ * have, the same window the door walks: enough to count them and to say a
33
+ * site is past the limit, never the whole screens collection.
34
+ */ export function useStarterPublishFacts(input) {
35
+ var _host_data;
36
+ const { hostId, orgId } = input;
37
+ const firestore = useFirestore();
38
+ const host = useFirestoreDoc(()=>hostId ? doc(firestore, 'hosts', hostId) : null, [
39
+ firestore,
40
+ hostId
41
+ ]);
42
+ const routing = (_host_data = host.data) == null ? void 0 : _host_data['screens'];
43
+ const routedIds = useMemo(()=>routing && typeof routing === 'object' && !Array.isArray(routing) ? Object.keys(routing).slice(0, SITE_STARTER_MAX_SCREENS + 1) : [], [
44
+ routing
45
+ ]);
46
+ const routedKey = routedIds.join('|');
47
+ const screens = useFirestoreCollection(()=>hostId && routedIds.length ? query(collection(firestore, 'hosts', hostId, 'screens'), where(documentId(), 'in', routedIds)) : null, // `routedKey` stands for the ids: a new array of the same ids is the same read.
48
+ [
49
+ firestore,
50
+ hostId,
51
+ routedKey
52
+ ], {
53
+ idField: '$id'
54
+ });
55
+ const profile = useFirestoreDoc(()=>orgId ? doc(firestore, 'publisherProfiles', orgId) : null, [
56
+ firestore,
57
+ orgId
58
+ ]);
59
+ const plan = usePlanFeature(hostId != null ? hostId : undefined, 'marketplaceSelling');
60
+ // Asked only of a profile with no payout account: a publisher that has one
61
+ // has nothing the answer could change, and nearly every publisher does.
62
+ const { data: user } = useUser();
63
+ const [sellsAsPlatform, setSellsAsPlatform] = useState(false);
64
+ const needsStanding = Boolean(profile.data && !payoutsReady(profile.data));
65
+ useEffect(()=>{
66
+ if (!needsStanding || !orgId || !user) return;
67
+ let current = true;
68
+ authorizedFetch(user, `/api/marketplace/publisher-standing?orgId=${encodeURIComponent(orgId)}`).then((response)=>response.ok ? response.json() : null).then((answer)=>{
69
+ if (current) setSellsAsPlatform((answer == null ? void 0 : answer.sellsAsPlatform) === true);
70
+ })// Not knowing leaves the payouts step up: the door has the last word.
71
+ .catch(()=>undefined);
72
+ return ()=>{
73
+ current = false;
74
+ };
75
+ }, [
76
+ needsStanding,
77
+ orgId,
78
+ user
79
+ ]);
80
+ const route = useConsoleHostRoute(hostId);
81
+ const hostReady = host.status === 'success';
82
+ const pagesReady = hostReady && (routedIds.length === 0 || screens.status === 'success');
83
+ return useMemo(()=>{
84
+ var _profile_data, _host_data;
85
+ return {
86
+ facts: {
87
+ plan,
88
+ profile: profile.status === 'success' ? (_profile_data = profile.data) != null ? _profile_data : null : undefined,
89
+ sellsAsPlatform,
90
+ pages: pagesReady ? starterPublishPages(routing, screens.data) : null,
91
+ marketplacePath: route.orgSlug ? orgMarketplacePath(route.orgSlug) : null,
92
+ pagesPath: route.orgSlug && route.subdomain ? buildRoute(Route.HOST_SCREENS, {
93
+ orgSlug: route.orgSlug,
94
+ host: route.subdomain
95
+ }) : null
96
+ },
97
+ builtin: hostReady ? siteStarterBuiltinThemeOf((_host_data = host.data) != null ? _host_data : null) : null,
98
+ hostReady
99
+ };
100
+ }, [
101
+ plan,
102
+ profile.status,
103
+ profile.data,
104
+ sellsAsPlatform,
105
+ pagesReady,
106
+ routing,
107
+ screens.data,
108
+ route.orgSlug,
109
+ route.subdomain,
110
+ hostReady,
111
+ host.data
112
+ ]);
113
+ }
114
+ export default useStarterPublishFacts;
115
+
116
+ //# sourceMappingURL=use-starter-publish-facts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/hooks/use-starter-publish-facts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { SITE_STARTER_MAX_SCREENS } from '@aglyn/aglyn/app-utils/site-starter'\nimport {\n siteStarterBuiltinThemeOf,\n type SiteStarterBuiltinReading,\n} from '@aglyn/aglyn/app-utils/site-starter-theme'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n usePlanFeature,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport { collection, doc, documentId, query, where } from 'firebase/firestore'\nimport { useEffect, useMemo, useState } from 'react'\nimport { orgMarketplacePath } from '../model/marketplace-paths'\nimport {\n payoutsReady,\n starterPublishPages,\n type StarterPublishFacts,\n type StarterPublishScreen,\n} from '../model/starter-publish'\n\n/** Everything \"Publish as a starter\" reads before the click. */\nexport interface StarterPublishFactsState {\n /** The reads; the form adds the price it holds. */\n facts: Omit<StarterPublishFacts, 'priceUsd'>\n /** The site's built-in theme as a starter would carry it, or `null` when it wears none. */\n builtin: SiteStarterBuiltinReading | null\n /** The host document has arrived, so `builtin` is an answer rather than a wait. */\n hostReady: boolean\n}\n\n/**\n * What a starter publish of one site would capture and what stands in its\n * way (AGL-4049): the host (its theme and routing), the routed pages it\n * would take, the publishing org's profile, and its plan.\n *\n * The routed pages are read by id, at most one past the most a starter may\n * have, the same window the door walks: enough to count them and to say a\n * site is past the limit, never the whole screens collection.\n */\nexport function useStarterPublishFacts(input: {\n hostId: string | null | undefined\n orgId: string | null | undefined\n}): StarterPublishFactsState {\n const { hostId, orgId } = input\n const firestore = useFirestore()\n const host = useFirestoreDoc<Record<string, unknown>>(\n () => (hostId ? doc(firestore, 'hosts', hostId) : null),\n [firestore, hostId],\n )\n const routing = host.data?.['screens']\n const routedIds = useMemo(\n () =>\n routing && typeof routing === 'object' && !Array.isArray(routing)\n ? Object.keys(routing).slice(0, SITE_STARTER_MAX_SCREENS + 1)\n : [],\n [routing],\n )\n const routedKey = routedIds.join('|')\n const screens = useFirestoreCollection<StarterPublishScreen>(\n () =>\n hostId && routedIds.length\n ? query(collection(firestore, 'hosts', hostId, 'screens'), where(documentId(), 'in', routedIds))\n : null,\n // `routedKey` stands for the ids: a new array of the same ids is the same read.\n [firestore, hostId, routedKey],\n { idField: '$id' },\n )\n const profile = useFirestoreDoc<Record<string, unknown>>(\n () => (orgId ? doc(firestore, 'publisherProfiles', orgId) : null),\n [firestore, orgId],\n )\n const plan = usePlanFeature(hostId ?? undefined, 'marketplaceSelling')\n // Asked only of a profile with no payout account: a publisher that has one\n // has nothing the answer could change, and nearly every publisher does.\n const { data: user } = useUser()\n const [sellsAsPlatform, setSellsAsPlatform] = useState(false)\n const needsStanding = Boolean(profile.data && !payoutsReady(profile.data))\n useEffect(() => {\n if (!needsStanding || !orgId || !user) return\n let current = true\n authorizedFetch(user, `/api/marketplace/publisher-standing?orgId=${encodeURIComponent(orgId)}`)\n .then((response) => (response.ok ? response.json() : null))\n .then((answer: { sellsAsPlatform?: unknown } | null) => {\n if (current) setSellsAsPlatform(answer?.sellsAsPlatform === true)\n })\n // Not knowing leaves the payouts step up: the door has the last word.\n .catch(() => undefined)\n return () => {\n current = false\n }\n }, [needsStanding, orgId, user])\n const route = useConsoleHostRoute(hostId)\n\n const hostReady = host.status === 'success'\n const pagesReady =\n hostReady && (routedIds.length === 0 || screens.status === 'success')\n return useMemo(\n () => ({\n facts: {\n plan,\n profile:\n profile.status === 'success' ? (profile.data ?? null) : undefined,\n sellsAsPlatform,\n pages: pagesReady ? starterPublishPages(routing, screens.data) : null,\n marketplacePath: route.orgSlug ? orgMarketplacePath(route.orgSlug) : null,\n pagesPath:\n route.orgSlug && route.subdomain\n ? buildRoute(Route.HOST_SCREENS, { orgSlug: route.orgSlug, host: route.subdomain })\n : null,\n },\n builtin: hostReady ? siteStarterBuiltinThemeOf(host.data ?? null) : null,\n hostReady,\n }),\n [plan, profile.status, profile.data, sellsAsPlatform, pagesReady, routing, screens.data, route.orgSlug, route.subdomain, hostReady, host.data],\n )\n}\n\nexport default useStarterPublishFacts\n"],"names":["buildRoute","Route","authorizedFetch","SITE_STARTER_MAX_SCREENS","siteStarterBuiltinThemeOf","useConsoleHostRoute","useFirestore","useFirestoreCollection","useFirestoreDoc","usePlanFeature","useUser","collection","doc","documentId","query","where","useEffect","useMemo","useState","orgMarketplacePath","payoutsReady","starterPublishPages","useStarterPublishFacts","input","host","hostId","orgId","firestore","routing","data","routedIds","Array","isArray","Object","keys","slice","routedKey","join","screens","length","idField","profile","plan","undefined","user","sellsAsPlatform","setSellsAsPlatform","needsStanding","Boolean","current","encodeURIComponent","then","response","ok","json","answer","catch","route","hostReady","status","pagesReady","facts","pages","marketplacePath","orgSlug","pagesPath","subdomain","HOST_SCREENS","builtin"],"mappings":"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"}
@@ -1,31 +1,26 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type PayListing } from './use-marketplace-pay';
1
3
  /** How one "Buy and continue" ended. */
2
4
  export type StarterPurchaseOutcome =
3
- /** The payment page is opening: the person comes back to Create site on the same pick. */
4
- 'opening'
5
- /** The workspace owns it already: nothing to pay, the site can be created. */
6
- | 'owned'
7
- /** It could not open; {@link StarterPurchase.error} says why. */
8
- | 'failed';
5
+ /** Paid just now, or owned already: the site can be created. */
6
+ 'owned'
7
+ /** Paid and still clearing: the site can be created once it has. */
8
+ | 'pending'
9
+ /** Closed, or it could not open; {@link StarterPurchase.error} says why. */
10
+ | 'stopped';
9
11
  export interface StarterPurchase {
10
- buy: (listingId: string) => Promise<StarterPurchaseOutcome>;
11
- /** A purchase is opening. */
12
+ buy: (listing: PayListing) => Promise<StarterPurchaseOutcome>;
13
+ /** A purchase is opening or paying. */
12
14
  busy: boolean;
13
15
  /** Why the last one did not open, or `null`. */
14
16
  error: string | null;
17
+ /** The payment, in place under the pick, or `null`. */
18
+ panel: ReactNode;
15
19
  }
16
- /** Where the browser goes to pay: replaced in a spec. */
17
- export declare const starterPurchaseNavigation: {
18
- assign: (url: string) => void;
19
- };
20
20
  /**
21
- * "Buy and continue" for a priced starter in Create site (AGL-4049): opens
22
- * the marketplace's payment for it, licensed to the workspace the site is
23
- * for, and names Create site as the way back, so the person returns to the
24
- * same pick with the site still to be created. Today's payment page, until
25
- * the marketplace pays in-app.
26
- *
27
- * One attempt per starter: the same key goes with a second click, so a
28
- * double click cannot open a second payment (AGL-1697). A workspace that
29
- * already owns it is told so, not charged again.
21
+ * "Buy and continue" for a priced starter in Create site (AGL-4049): pays in
22
+ * the console, licensed to the workspace the site is for, so the person stays
23
+ * on the same pick with the site still to be created. The purchase is on
24
+ * record when it answers, so Create is ready on the next click.
30
25
  */
31
- export declare function useStarterPurchase(orgId: string | undefined): StarterPurchase;
26
+ export declare function useStarterPurchase(orgId: string | undefined, orgSlug: string): StarterPurchase;