@aglyn/plugins-marketplace 1.1.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.
- package/README.md +1 -1
- package/package.json +14 -14
- package/src/lib/components/listing-content.component.js +11 -3
- package/src/lib/components/listing-content.component.js.map +1 -1
- package/src/lib/components/marketplace-pay-panel.component.d.ts +67 -0
- package/src/lib/components/marketplace-pay-panel.component.js +228 -0
- package/src/lib/components/marketplace-pay-panel.component.js.map +1 -0
- package/src/lib/components/publish-artifact-dialog.component.d.ts +1 -1
- package/src/lib/components/publish-artifact-dialog.component.js +144 -20
- package/src/lib/components/publish-artifact-dialog.component.js.map +1 -1
- package/src/lib/components/publish-starter-fields.component.d.ts +35 -0
- package/src/lib/components/publish-starter-fields.component.js +176 -0
- package/src/lib/components/publish-starter-fields.component.js.map +1 -0
- package/src/lib/components/starter-picker-shelf.component.d.ts +2 -2
- package/src/lib/components/starter-picker-shelf.component.js +28 -6
- package/src/lib/components/starter-picker-shelf.component.js.map +1 -1
- package/src/lib/hooks/use-marketplace-actions.d.ts +9 -3
- package/src/lib/hooks/use-marketplace-actions.js +47 -89
- package/src/lib/hooks/use-marketplace-actions.js.map +1 -1
- package/src/lib/hooks/use-marketplace-pay.d.ts +70 -0
- package/src/lib/hooks/use-marketplace-pay.js +359 -0
- package/src/lib/hooks/use-marketplace-pay.js.map +1 -0
- package/src/lib/hooks/use-starter-publish-facts.d.ts +25 -0
- package/src/lib/hooks/use-starter-publish-facts.js +116 -0
- package/src/lib/hooks/use-starter-publish-facts.js.map +1 -0
- package/src/lib/hooks/use-starter-purchase.d.ts +17 -22
- package/src/lib/hooks/use-starter-purchase.js +26 -73
- package/src/lib/hooks/use-starter-purchase.js.map +1 -1
- package/src/lib/model/marketplace-invoice.d.ts +52 -0
- package/src/lib/model/marketplace-invoice.js +75 -0
- package/src/lib/model/marketplace-invoice.js.map +1 -0
- package/src/lib/model/marketplace-pay.d.ts +42 -0
- package/src/lib/model/marketplace-pay.js +33 -0
- package/src/lib/model/marketplace-pay.js.map +1 -0
- package/src/lib/model/platform-seller.d.ts +37 -0
- package/src/lib/model/platform-seller.js +36 -0
- package/src/lib/model/platform-seller.js.map +1 -0
- package/src/lib/model/starter-publish.d.ts +159 -0
- package/src/lib/model/starter-publish.js +189 -0
- package/src/lib/model/starter-publish.js.map +1 -0
- package/src/lib/model/starter-query.d.ts +0 -8
- package/src/lib/model/starter-query.js +0 -10
- package/src/lib/model/starter-query.js.map +1 -1
- package/src/lib/server/billing-webhook.d.ts +6 -0
- package/src/lib/server/billing-webhook.js +269 -211
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/checkout.d.ts +2 -48
- package/src/lib/server/checkout.js +21 -241
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/pay.d.ts +18 -0
- package/src/lib/server/pay.js +540 -0
- package/src/lib/server/pay.js.map +1 -0
- package/src/lib/server/platform-org.d.ts +23 -0
- package/src/lib/server/platform-org.js +27 -0
- package/src/lib/server/platform-org.js.map +1 -0
- package/src/lib/server/publish-preconditions.js +4 -1
- package/src/lib/server/publish-preconditions.js.map +1 -1
- package/src/lib/server/publish-starter.js +5 -2
- package/src/lib/server/publish-starter.js.map +1 -1
- package/src/lib/server/publisher-profile.d.ts +5 -0
- package/src/lib/server/publisher-profile.js +20 -1
- package/src/lib/server/publisher-profile.js.map +1 -1
- package/src/lib/server/publisher-standing.d.ts +26 -0
- package/src/lib/server/publisher-standing.js +66 -0
- package/src/lib/server/publisher-standing.js.map +1 -0
- package/src/lib/server/sale.d.ts +119 -0
- package/src/lib/server/sale.js +304 -0
- package/src/lib/server/sale.js.map +1 -0
- package/src/lib/server/starter-publish-facts.d.ts +36 -0
- package/src/lib/server/starter-publish-facts.js +135 -0
- package/src/lib/server/starter-publish-facts.js.map +1 -0
- package/src/lib/server.js +8 -0
- package/src/lib/server.js.map +1 -1
|
@@ -2,10 +2,14 @@ import { type InstallPlanStep } from '../model/marketplace';
|
|
|
2
2
|
/**
|
|
3
3
|
* Shared install/buy handlers for marketplace listings (AGL-95): used by the
|
|
4
4
|
* browse grid and the listing detail page. Install copies the pinned
|
|
5
|
-
* version snapshot server-side (/api/marketplace/install); buy
|
|
6
|
-
*
|
|
5
|
+
* version snapshot server-side (/api/marketplace/install); buy pays in the
|
|
6
|
+
* console (/api/marketplace/pay), in the `payPanel` the surface draws under
|
|
7
|
+
* its button.
|
|
8
|
+
*
|
|
9
|
+
* @param billingHref the workspace's Billing page, for a prerequisite the
|
|
10
|
+
* buyer cannot meet themselves.
|
|
7
11
|
*/
|
|
8
|
-
export declare function useMarketplaceActions(hostId: string, orgId?: string | null): {
|
|
12
|
+
export declare function useMarketplaceActions(hostId: string, orgId?: string | null, billingHref?: string | null): {
|
|
9
13
|
install: (listing: any, scope?: "org" | "host", options?: {
|
|
10
14
|
/**
|
|
11
15
|
* Called instead of an error toast when the install route refuses
|
|
@@ -33,5 +37,7 @@ export declare function useMarketplaceActions(hostId: string, orgId?: string | n
|
|
|
33
37
|
}) => Promise<void>;
|
|
34
38
|
buy: (listing: any) => Promise<any>;
|
|
35
39
|
uninstall: (listing: any, scope?: "org" | "host", targetHostId?: string) => Promise<any>;
|
|
40
|
+
payPanel: import("react").ReactNode;
|
|
41
|
+
buying: boolean;
|
|
36
42
|
};
|
|
37
43
|
export default useMarketplaceActions;
|
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
18
|
import { lockdownRefusalText, parseLockdownRefusal } from "@aglyn/aglyn";
|
|
19
|
-
import { readGaClientId } from "@aglyn/aglyn/app-utils/analytics-events";
|
|
20
19
|
import { useLoading } from "@aglyn/shared-ui-jsx";
|
|
21
20
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
22
|
-
import { useCallback
|
|
21
|
+
import { useCallback } from "react";
|
|
23
22
|
import { useUser } from "@aglyn/tenant-feature-instance";
|
|
24
23
|
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
25
24
|
import { listingArtifactType } from "../model/marketplace.js";
|
|
26
25
|
import { installPlanOutcome, marketplaceInstallEndpoint, marketplaceLandingMessage } from "../model/marketplace-catalog.js";
|
|
26
|
+
import { useMarketplacePay } from "./use-marketplace-pay.js";
|
|
27
27
|
/**
|
|
28
28
|
* What to tell someone whose install will never be three-way mergeable
|
|
29
29
|
* (AGL-2339).
|
|
@@ -50,25 +50,21 @@ import { installPlanOutcome, marketplaceInstallEndpoint, marketplaceLandingMessa
|
|
|
50
50
|
/**
|
|
51
51
|
* Shared install/buy handlers for marketplace listings (AGL-95): used by the
|
|
52
52
|
* browse grid and the listing detail page. Install copies the pinned
|
|
53
|
-
* version snapshot server-side (/api/marketplace/install); buy
|
|
54
|
-
*
|
|
55
|
-
|
|
53
|
+
* version snapshot server-side (/api/marketplace/install); buy pays in the
|
|
54
|
+
* console (/api/marketplace/pay), in the `payPanel` the surface draws under
|
|
55
|
+
* its button.
|
|
56
|
+
*
|
|
57
|
+
* @param billingHref the workspace's Billing page, for a prerequisite the
|
|
58
|
+
* buyer cannot meet themselves.
|
|
59
|
+
*/ export function useMarketplaceActions(hostId, orgId, billingHref) {
|
|
56
60
|
const { data: user } = useUser();
|
|
57
61
|
const { enqueueSnackbar } = useSnackbar();
|
|
58
62
|
const { queueLoading } = useLoading();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
* see two purchases. Held in a ref rather than state so a re-render cannot
|
|
65
|
-
* rotate a key mid-flight.
|
|
66
|
-
*
|
|
67
|
-
* It retires when the page does. That is the honest boundary for a redirect
|
|
68
|
-
* flow — the buyer leaves for Stripe and comes back to a fresh mount — and it
|
|
69
|
-
* means this key covers the double-click and the retry, while the server's
|
|
70
|
-
* already-purchased check covers the buyer who returns tomorrow.
|
|
71
|
-
*/ const purchaseAttempts = useRef(new Map());
|
|
63
|
+
const pay = useMarketplacePay({
|
|
64
|
+
orgId,
|
|
65
|
+
hostId,
|
|
66
|
+
billingHref
|
|
67
|
+
});
|
|
72
68
|
// `scope` is only meaningful for artifact types that HAVE an org-scoped
|
|
73
69
|
// pin — see INSTALL_TARGETS. Passing it for a template would be ignored
|
|
74
70
|
// server-side, so callers ask the model rather than guessing.
|
|
@@ -313,82 +309,42 @@ import { installPlanOutcome, marketplaceInstallEndpoint, marketplaceLandingMessa
|
|
|
313
309
|
enqueueSnackbar
|
|
314
310
|
]);
|
|
315
311
|
const buy = useCallback(async (listing)=>{
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
// Stable across a retry of THIS attempt (AGL-1697), so a
|
|
327
|
-
// double-click or a re-submit after a timeout cannot open a
|
|
328
|
-
// second Stripe session on a live account.
|
|
329
|
-
'Idempotency-Key': attemptKey
|
|
330
|
-
},
|
|
331
|
-
// The browser's GA client id rides along so the SERVER-side
|
|
332
|
-
// `purchase` the Stripe webhook sends can be attributed to the
|
|
333
|
-
// session — and therefore the campaign — that produced it
|
|
334
|
-
// (AGL-1638). The webhook has always read this off the session
|
|
335
|
-
// metadata; until now nothing captured it, so every marketplace
|
|
336
|
-
// sale landed on a synthetic, sessionless GA user and plugin
|
|
337
|
-
// revenue had no acquisition channel at all.
|
|
338
|
-
//
|
|
339
|
-
// Resolves to null within 500ms when gtag is absent (consent
|
|
340
|
-
// refused, ad blocker, analytics unconfigured), so it can never
|
|
341
|
-
// delay or block a purchase — same contract as the console's
|
|
342
|
-
// subscription checkout.
|
|
343
|
-
body: JSON.stringify(_extends({
|
|
344
|
-
listingId: listing.$id,
|
|
345
|
-
hostId
|
|
346
|
-
}, orgId ? {
|
|
347
|
-
orgId
|
|
348
|
-
} : {}, {
|
|
349
|
-
gaClientId: await readGaClientId(process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID)
|
|
350
|
-
}))
|
|
312
|
+
var _listing_displayName;
|
|
313
|
+
const name = String((_listing_displayName = listing.displayName) != null ? _listing_displayName : 'This listing');
|
|
314
|
+
const { outcome, error, locked } = await pay.buy({
|
|
315
|
+
listingId: String(listing.$id),
|
|
316
|
+
displayName: listing.displayName
|
|
317
|
+
});
|
|
318
|
+
if (outcome === 'paid') {
|
|
319
|
+
return void enqueueSnackbar(`Bought "${name}". It is yours to install.`, {
|
|
320
|
+
variant: 'success',
|
|
321
|
+
persist: false
|
|
351
322
|
});
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
var _ref;
|
|
372
|
-
return void enqueueSnackbar((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'Checkout failed', {
|
|
373
|
-
variant: 'error',
|
|
374
|
-
allowDuplicate: true
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
window.location.assign(payload.url);
|
|
378
|
-
} catch (error) {
|
|
379
|
-
console.error(error);
|
|
380
|
-
enqueueSnackbar('An error has occurred', {
|
|
323
|
+
}
|
|
324
|
+
if (outcome === 'owned') {
|
|
325
|
+
return void enqueueSnackbar(`This organization already owns "${name}".`, {
|
|
326
|
+
variant: 'info',
|
|
327
|
+
persist: false
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
if (outcome === 'pending') {
|
|
331
|
+
return void enqueueSnackbar(`Your payment for "${name}" is going through. It is yours as soon as it clears.`, {
|
|
332
|
+
variant: 'info',
|
|
333
|
+
persist: false
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
// A lockdown is a pause to read, and it must not vanish unread (AGL-1532).
|
|
337
|
+
if (error) {
|
|
338
|
+
enqueueSnackbar(error, locked ? {
|
|
339
|
+
variant: 'warning',
|
|
340
|
+
persist: true
|
|
341
|
+
} : {
|
|
381
342
|
variant: 'error',
|
|
382
343
|
allowDuplicate: true
|
|
383
344
|
});
|
|
384
|
-
} finally{
|
|
385
|
-
dequeue();
|
|
386
345
|
}
|
|
387
346
|
}, [
|
|
388
|
-
|
|
389
|
-
hostId,
|
|
390
|
-
orgId,
|
|
391
|
-
queueLoading,
|
|
347
|
+
pay.buy,
|
|
392
348
|
enqueueSnackbar
|
|
393
349
|
]);
|
|
394
350
|
/**
|
|
@@ -459,7 +415,9 @@ import { installPlanOutcome, marketplaceInstallEndpoint, marketplaceLandingMessa
|
|
|
459
415
|
install,
|
|
460
416
|
installPlan,
|
|
461
417
|
buy,
|
|
462
|
-
uninstall
|
|
418
|
+
uninstall,
|
|
419
|
+
payPanel: pay.panel,
|
|
420
|
+
buying: pay.busy
|
|
463
421
|
};
|
|
464
422
|
}
|
|
465
423
|
export default useMarketplaceActions;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/hooks/use-marketplace-actions.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 { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport { readGaClientId } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { useLoading } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useCallback, useRef } from 'react'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n type InstallPlanStep,\n listingArtifactType,\n} from '../model/marketplace'\nimport {\n installPlanOutcome,\n marketplaceInstallEndpoint,\n marketplaceLandingMessage,\n} from '../model/marketplace-catalog'\n\n\n/**\n * What to tell someone whose install will never be three-way mergeable\n * (AGL-2339).\n *\n * Every install route returns `baseStored`, and `provenance.ts` says what a\n * false one means: *\"a snapshot that cannot be written leaves `baseStored:\n * false` and an artifact that reports itself as not updatable\"*. It reported\n * it to NOBODY — zero `.tsx` readers across all seven routes.\n *\n * The consequence is silent and expensive. `hasDivergedFromBase` needs the\n * base snapshot to tell the publisher's change from the user's; without one\n * an update can only overwrite. So a user who customizes a large artifact —\n * one whose content exceeded `ARTIFACT_BASE_MAX_BYTES`, or whose snapshot\n * write failed — LOSES those changes on the next update, having been told\n * nothing at install time. The update dialog does explain itself when the\n * moment arrives (`preview.mergeable` renders `preview.reason`), but by then\n * the choice is take-the-update-or-stay-behind, not \"keep a copy first\".\n *\n * Persistent rather than a timed toast: it is advice to act on before\n * editing, and it is the only moment this is knowable in advance.\n */\nfunction baseMissingWarning(displayName: string): string {\n return (\n `\"${displayName}\" is too large to keep a reference copy of, so a future ` +\n 'update will REPLACE it rather than merge. Export or note any changes ' +\n 'you make to it.'\n )\n}\n\n/**\n * Shared install/buy handlers for marketplace listings (AGL-95): used by the\n * browse grid and the listing detail page. Install copies the pinned\n * version snapshot server-side (/api/marketplace/install); buy opens a\n * Stripe checkout (/api/marketplace/checkout) when configured.\n */\nexport function useMarketplaceActions(hostId: string, orgId?: string | null) {\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { queueLoading } = useLoading()\n\n /**\n * One purchase attempt per listing, minted lazily (AGL-1697).\n *\n * Per-listing rather than per-click: minting inside `buy` would defeat the\n * whole point, because two clicks would mint two keys and the server would\n * see two purchases. Held in a ref rather than state so a re-render cannot\n * rotate a key mid-flight.\n *\n * It retires when the page does. That is the honest boundary for a redirect\n * flow — the buyer leaves for Stripe and comes back to a fresh mount — and it\n * means this key covers the double-click and the retry, while the server's\n * already-purchased check covers the buyer who returns tomorrow.\n */\n const purchaseAttempts = useRef(new Map<string, string>())\n\n // `scope` is only meaningful for artifact types that HAVE an org-scoped\n // pin — see INSTALL_TARGETS. Passing it for a template would be ignored\n // server-side, so callers ask the model rather than guessing.\n const install = useCallback(\n async (\n listing: any,\n scope?: 'org' | 'host',\n options?: {\n /**\n * Called instead of an error toast when the install route refuses\n * because the workspace's copy has been EDITED since it was installed\n * (AGL-1018's 409, AGL-1034).\n *\n * The refusal is correct — that install would have destroyed their\n * work — but \"review the update\" is only useful advice if something\n * offers the review. Handling it here rather than at each call site\n * means every surface that installs a copied artifact gets the\n * recovery, including ones written later.\n */\n onDiverged?: () => void\n },\n ) => {\n const dequeue = queueLoading()\n try {\n const artifactType = listingArtifactType(listing)\n const endpoint = marketplaceInstallEndpoint(artifactType)\n const response = await authorizedFetch(user, `/api/${endpoint}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n listingId: listing.$id,\n hostId,\n ...(scope ? { scope } : {}),\n }),\n })\n const payload = await response.json()\n if (!response.ok) {\n // The copy has diverged: hand it to the review rather than telling\n // someone to review something they cannot reach from here.\n if (payload?.diverged && options?.onDiverged) {\n return void options.onDiverged()\n }\n // An installs feature lockdown (AGL-1510/1532): \"Install failed\"\n // reads as a broken listing — the very thing an incident response\n // needs people NOT to conclude while a suspect listing is under\n // investigation. The server says installs are paused; say that.\n const locked = parseLockdownRefusal(response.status, payload)\n if (locked) {\n return void enqueueSnackbar(lockdownRefusalText(locked), {\n variant: 'warning',\n persist: true,\n })\n }\n return void enqueueSnackbar(payload?.error ?? 'Install failed', {\n variant: response.status === 402 ? 'warning' : 'error',\n allowDuplicate: true,\n })\n }\n // Types that land inert must not read as \"your site changed\".\n const landed = marketplaceLandingMessage(artifactType, listing.displayName)\n enqueueSnackbar(\n landed ??\n (payload.updated\n ? `Updated \"${listing.displayName}\" to v${payload.version}`\n : `Installed \"${listing.displayName}\" — find it under Your components`),\n { variant: 'success', persist: false },\n )\n // No base snapshot: this copy can never be three-way merged, and a\n // future update will overwrite whatever the user does to it. The\n // route has always said so on the wire; nothing listened (AGL-2339).\n //\n // `=== false` rather than a falsy test: a route that omitted the\n // field entirely has told us nothing, and warning on silence would\n // put a scary, unactionable message on every install from any\n // endpoint that had not been updated yet.\n if (payload.baseStored === false) {\n enqueueSnackbar(baseMissingWarning(listing.displayName), {\n variant: 'warning',\n persist: true,\n })\n }\n /**\n * What was, and was not, checked about an email somebody else wrote.\n *\n * Said HERE because this is the moment the design enters the\n * workspace, and because the two facts are the ones a sender cannot\n * recover afterwards by looking at the design. `unreviewed` is the\n * ordinary case — email starters are auto-listed like every other\n * copied artifact — so the sentence has to be a statement rather than\n * an alarm, and it must never be silence: an install that said nothing\n * would be read as a review that happened.\n *\n * The link hosts are the inspectable half of the link policy. The\n * remote-asset rule is mechanical and the route enforces it, but where\n * a template points a tenant's own customers is a judgment only the\n * tenant can make, and they can only make it if they are shown the\n * list.\n */\n if (payload.assurance) {\n const hosts: string[] = payload.linkHosts ?? []\n enqueueSnackbar(\n (payload.assurance === 'approved'\n ? 'Reviewed by Aglyn. '\n : 'Nobody at Aglyn has reviewed this design. ') +\n (hosts.length\n ? `It links to ${hosts.join(', ')}. `\n : 'It links nowhere outside your site. ') +\n 'Read it before you send from it.',\n { variant: 'info', persist: true },\n )\n }\n // A media reference that still resolves against the publisher's own\n // library — served by our CDN, so no third party learns who opened the\n // mail, but the picture behind it is still theirs to change.\n if (payload.foreignMediaScopes?.length) {\n enqueueSnackbar(\n 'Some images in this email are still served from the publisher’s ' +\n 'own media library, so they can change after you install. ' +\n 'Replace them with your own to pin them.',\n { variant: 'info', persist: false },\n )\n }\n // A schema whose reference fields couldn't be relinked installs with\n // those fields degraded to text — silently changing a field's type\n // would be the kind of thing you discover much later (AGL-657).\n if (payload.degradedFieldIds?.length) {\n enqueueSnackbar(\n `${payload.degradedFieldIds.length} reference field(s) became ` +\n 'plain text — the datasets they pointed at are not in this ' +\n 'organization.',\n { variant: 'info', persist: false },\n )\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, queueLoading, enqueueSnackbar],\n )\n\n /**\n * Fans an install-targeting plan out to the install routes (AGL-773). Each\n * step is either a single org pin or a host pin on a named site; org steps\n * resolve the org from the acting `hostId`. One summary snackbar reports the\n * whole plan rather than N toasts. Use `resolveInstallPlan` (model) to turn\n * an All-sites / Selected-sites choice into these steps.\n */\n const installPlan = useCallback(\n async (\n listing: any,\n steps: readonly InstallPlanStep[],\n options?: {\n /**\n * Re-pinning something already installed (AGL-1017). Only the wording\n * changes — the route is the same, because moving a pin forward IS\n * installing — but \"Installed on 1 site\" is the wrong sentence to read\n * after pressing Update.\n */\n intent?: 'install' | 'update'\n /** The version being moved to, for the update summary. */\n version?: string | number | null\n },\n ) => {\n if (!steps.length) return\n const dequeue = queueLoading()\n try {\n const artifactType = listingArtifactType(listing)\n const endpoint = marketplaceInstallEndpoint(artifactType)\n let installed = 0\n const errors: string[] = []\n /**\n * Surfaced separately from success (AGL-1017/429): the pin is written\n * either way — it is an explicit choice — but a bundle built for a\n * different host ABI will not load, and a green \"Updated\" toast alone\n * would leave the workspace believing it did.\n */\n const abiWarnings = new Set<string>()\n /**\n * Sites whose copy landed with no base snapshot (AGL-2339). Counted\n * rather than toasted per step: the fan-out installs the SAME\n * artifact everywhere, so the answer is the same on every site and N\n * identical warnings would read as N different problems.\n */\n let withoutBase = 0\n /**\n * A feature lockdown refuses every step identically (AGL-1532), so\n * the fan-out stops at the first one and says \"installs are paused\"\n * ONCE. N identical refusals stacked into the error summary would\n * read as N broken sites — the opposite of what the lock is saying.\n */\n let lockedNotice: ReturnType<typeof parseLockdownRefusal> = null\n for (const step of steps) {\n // Org steps still need a host to resolve the org server-side, so\n // fall back to the acting host; host steps target their own site.\n const targetHostId = step.scope === 'host' ? step.hostId : hostId\n const response = await authorizedFetch(user, `/api/${endpoint}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n listingId: listing.$id,\n hostId: targetHostId,\n ...(step.scope === 'org' ? { scope: 'org' } : {}),\n }),\n })\n const payload = await response.json().catch(() => ({}))\n if (response.ok) {\n installed += 1\n if (payload?.hostAbiWarning) {\n abiWarnings.add(String(payload.hostAbiWarning))\n }\n if (payload?.baseStored === false) withoutBase += 1\n } else {\n lockedNotice = parseLockdownRefusal(response.status, payload)\n if (lockedNotice) break\n errors.push(payload?.error ?? 'Install failed')\n }\n }\n if (lockedNotice) {\n enqueueSnackbar(lockdownRefusalText(lockedNotice), {\n variant: 'warning',\n persist: true,\n })\n // Whatever landed before the lock DID land — saying so is the\n // difference between \"nothing happened\" and a half-applied plan\n // the workspace would otherwise discover much later.\n if (installed) {\n enqueueSnackbar(\n `${installed} site(s) were already updated before the pause.`,\n { variant: 'info', persist: false },\n )\n }\n return\n }\n const outcome = installPlanOutcome({\n name: listing.displayName,\n installed,\n errors,\n orgWide: steps.some((step) => step.scope === 'org'),\n updating: options?.intent === 'update',\n version: options?.version != null ? String(options.version) : null,\n saved: Boolean(marketplaceLandingMessage(artifactType, '')),\n })\n enqueueSnackbar(\n outcome.text,\n outcome.tone === 'success'\n ? { variant: 'success', persist: false }\n : { variant: outcome.tone, allowDuplicate: true },\n )\n for (const warning of abiWarnings) {\n enqueueSnackbar(warning, { variant: 'warning', allowDuplicate: true })\n }\n if (withoutBase) {\n enqueueSnackbar(baseMissingWarning(listing.displayName), {\n variant: 'warning',\n persist: true,\n })\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, queueLoading, enqueueSnackbar],\n )\n\n const buy = useCallback(\n async (listing: any) => {\n const dequeue = queueLoading()\n try {\n const listingId = String(listing.$id)\n const attemptKey =\n purchaseAttempts.current.get(listingId) ??\n `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n purchaseAttempts.current.set(listingId, attemptKey)\n const response = await authorizedFetch(\n user,\n '/api/marketplace/checkout',\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n // Stable across a retry of THIS attempt (AGL-1697), so a\n // double-click or a re-submit after a timeout cannot open a\n // second Stripe session on a live account.\n 'Idempotency-Key': attemptKey,\n },\n // The browser's GA client id rides along so the SERVER-side\n // `purchase` the Stripe webhook sends can be attributed to the\n // session — and therefore the campaign — that produced it\n // (AGL-1638). The webhook has always read this off the session\n // metadata; until now nothing captured it, so every marketplace\n // sale landed on a synthetic, sessionless GA user and plugin\n // revenue had no acquisition channel at all.\n //\n // Resolves to null within 500ms when gtag is absent (consent\n // refused, ad blocker, analytics unconfigured), so it can never\n // delay or block a purchase — same contract as the console's\n // subscription checkout.\n body: JSON.stringify({\n listingId: listing.$id,\n hostId,\n // WHICH WORKSPACE IS BUYING (AGL-2331). A purchase licenses an\n // organization, so the acting org is named rather than inferred:\n // at org scope the surface acts through an arbitrary FIRST site,\n // and letting the server derive the licence holder from that site\n // would make which client workspace got the licence depend on the\n // order sites happen to come back in.\n //\n // Advisory, not authority — the server resolves it through the\n // caller's own membership and refuses an org they are not in. An\n // older cached bundle that sends none still works: the server\n // falls back to resolving the org from `hostId` the same way.\n ...(orgId ? { orgId } : {}),\n gaClientId: await readGaClientId(\n process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,\n ),\n }),\n },\n )\n const payload = await response.json()\n // The PAID install door refuses under a lock too (AGL-1545): both\n // `checkout` and `marketplace-installs` gate it. A buyer must not\n // read either lock as a payment failure — same reason the billing\n // page carries this branch (AGL-1532).\n const locked = parseLockdownRefusal(response.status, payload)\n if (locked) {\n return void enqueueSnackbar(lockdownRefusalText(locked), {\n variant: 'warning',\n persist: true,\n })\n }\n if (response.status === 501) {\n return void enqueueSnackbar(\n 'Purchases are not configured on this deployment',\n { variant: 'info', persist: false },\n )\n }\n if (!response.ok || !payload?.url) {\n return void enqueueSnackbar(payload?.error ?? 'Checkout failed', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n window.location.assign(payload.url)\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, orgId, queueLoading, enqueueSnackbar],\n )\n\n /**\n * Uninstall a plugin listing from the listing detail (AGL-881) — the same\n * `action:'uninstall'` the installed add-ons card uses, so it removes the\n * pin (and the switchboard entry when nothing else pins it) and decrements\n * activeInstalls. Only plugins have a removable pin; other artifact types\n * land in a library and have nothing to uninstall.\n */\n const uninstall = useCallback(\n async (\n listing: any,\n scope?: 'org' | 'host',\n // Which site's pin to drop (AGL-997). Without this the call always\n // targeted the acting host, so at org scope \"uninstall\" could only\n // mean \"the whole org pin\" or \"whichever site happened to be acting\"\n // — removing the plugin from ONE of several sites was unreachable.\n targetHostId?: string,\n ) => {\n const dequeue = queueLoading()\n try {\n const response = await authorizedFetch(\n user,\n '/api/marketplace/install-plugin',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n listingId: listing.$id,\n hostId: targetHostId ?? hostId,\n action: 'uninstall',\n ...(scope ? { scope } : {}),\n }),\n },\n )\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n // Uninstall rides the install-plugin route, so an installs lock\n // refuses it too (AGL-1532) — and \"Uninstall failed\" during an\n // incident is exactly the wrong sentence for someone trying to\n // remove the listing under investigation.\n const locked = parseLockdownRefusal(response.status, payload)\n if (locked) {\n return void enqueueSnackbar(lockdownRefusalText(locked), {\n variant: 'warning',\n persist: true,\n })\n }\n return void enqueueSnackbar(payload?.error ?? 'Uninstall failed', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n enqueueSnackbar(`Removed \"${listing.displayName}\"`, {\n variant: 'success',\n persist: false,\n })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, queueLoading, enqueueSnackbar],\n )\n\n return { install, installPlan, buy, uninstall }\n}\n\nexport default useMarketplaceActions\n"],"names":["lockdownRefusalText","parseLockdownRefusal","readGaClientId","useLoading","useSnackbar","useCallback","useRef","useUser","authorizedFetch","listingArtifactType","installPlanOutcome","marketplaceInstallEndpoint","marketplaceLandingMessage","baseMissingWarning","displayName","useMarketplaceActions","hostId","orgId","data","user","enqueueSnackbar","queueLoading","purchaseAttempts","Map","install","listing","scope","options","dequeue","payload","artifactType","endpoint","response","method","headers","body","JSON","stringify","listingId","$id","json","ok","diverged","onDiverged","locked","status","variant","persist","error","allowDuplicate","landed","updated","version","baseStored","assurance","hosts","linkHosts","length","join","foreignMediaScopes","degradedFieldIds","console","installPlan","steps","installed","errors","abiWarnings","Set","withoutBase","lockedNotice","step","targetHostId","catch","hostAbiWarning","add","String","push","outcome","name","orgWide","some","updating","intent","saved","Boolean","text","tone","warning","buy","attemptKey","current","get","Date","now","toString","Math","random","slice","set","gaClientId","process","env","NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID","url","window","location","assign","uninstall","action"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/hooks/use-marketplace-actions.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 { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport { useLoading } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useCallback } from 'react'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n type InstallPlanStep,\n listingArtifactType,\n} from '../model/marketplace'\nimport {\n installPlanOutcome,\n marketplaceInstallEndpoint,\n marketplaceLandingMessage,\n} from '../model/marketplace-catalog'\nimport { useMarketplacePay } from './use-marketplace-pay'\n\n\n/**\n * What to tell someone whose install will never be three-way mergeable\n * (AGL-2339).\n *\n * Every install route returns `baseStored`, and `provenance.ts` says what a\n * false one means: *\"a snapshot that cannot be written leaves `baseStored:\n * false` and an artifact that reports itself as not updatable\"*. It reported\n * it to NOBODY — zero `.tsx` readers across all seven routes.\n *\n * The consequence is silent and expensive. `hasDivergedFromBase` needs the\n * base snapshot to tell the publisher's change from the user's; without one\n * an update can only overwrite. So a user who customizes a large artifact —\n * one whose content exceeded `ARTIFACT_BASE_MAX_BYTES`, or whose snapshot\n * write failed — LOSES those changes on the next update, having been told\n * nothing at install time. The update dialog does explain itself when the\n * moment arrives (`preview.mergeable` renders `preview.reason`), but by then\n * the choice is take-the-update-or-stay-behind, not \"keep a copy first\".\n *\n * Persistent rather than a timed toast: it is advice to act on before\n * editing, and it is the only moment this is knowable in advance.\n */\nfunction baseMissingWarning(displayName: string): string {\n return (\n `\"${displayName}\" is too large to keep a reference copy of, so a future ` +\n 'update will REPLACE it rather than merge. Export or note any changes ' +\n 'you make to it.'\n )\n}\n\n/**\n * Shared install/buy handlers for marketplace listings (AGL-95): used by the\n * browse grid and the listing detail page. Install copies the pinned\n * version snapshot server-side (/api/marketplace/install); buy pays in the\n * console (/api/marketplace/pay), in the `payPanel` the surface draws under\n * its button.\n *\n * @param billingHref the workspace's Billing page, for a prerequisite the\n * buyer cannot meet themselves.\n */\nexport function useMarketplaceActions(\n hostId: string,\n orgId?: string | null,\n billingHref?: string | null,\n) {\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { queueLoading } = useLoading()\n const pay = useMarketplacePay({ orgId, hostId, billingHref })\n\n // `scope` is only meaningful for artifact types that HAVE an org-scoped\n // pin — see INSTALL_TARGETS. Passing it for a template would be ignored\n // server-side, so callers ask the model rather than guessing.\n const install = useCallback(\n async (\n listing: any,\n scope?: 'org' | 'host',\n options?: {\n /**\n * Called instead of an error toast when the install route refuses\n * because the workspace's copy has been EDITED since it was installed\n * (AGL-1018's 409, AGL-1034).\n *\n * The refusal is correct — that install would have destroyed their\n * work — but \"review the update\" is only useful advice if something\n * offers the review. Handling it here rather than at each call site\n * means every surface that installs a copied artifact gets the\n * recovery, including ones written later.\n */\n onDiverged?: () => void\n },\n ) => {\n const dequeue = queueLoading()\n try {\n const artifactType = listingArtifactType(listing)\n const endpoint = marketplaceInstallEndpoint(artifactType)\n const response = await authorizedFetch(user, `/api/${endpoint}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n listingId: listing.$id,\n hostId,\n ...(scope ? { scope } : {}),\n }),\n })\n const payload = await response.json()\n if (!response.ok) {\n // The copy has diverged: hand it to the review rather than telling\n // someone to review something they cannot reach from here.\n if (payload?.diverged && options?.onDiverged) {\n return void options.onDiverged()\n }\n // An installs feature lockdown (AGL-1510/1532): \"Install failed\"\n // reads as a broken listing — the very thing an incident response\n // needs people NOT to conclude while a suspect listing is under\n // investigation. The server says installs are paused; say that.\n const locked = parseLockdownRefusal(response.status, payload)\n if (locked) {\n return void enqueueSnackbar(lockdownRefusalText(locked), {\n variant: 'warning',\n persist: true,\n })\n }\n return void enqueueSnackbar(payload?.error ?? 'Install failed', {\n variant: response.status === 402 ? 'warning' : 'error',\n allowDuplicate: true,\n })\n }\n // Types that land inert must not read as \"your site changed\".\n const landed = marketplaceLandingMessage(artifactType, listing.displayName)\n enqueueSnackbar(\n landed ??\n (payload.updated\n ? `Updated \"${listing.displayName}\" to v${payload.version}`\n : `Installed \"${listing.displayName}\" — find it under Your components`),\n { variant: 'success', persist: false },\n )\n // No base snapshot: this copy can never be three-way merged, and a\n // future update will overwrite whatever the user does to it. The\n // route has always said so on the wire; nothing listened (AGL-2339).\n //\n // `=== false` rather than a falsy test: a route that omitted the\n // field entirely has told us nothing, and warning on silence would\n // put a scary, unactionable message on every install from any\n // endpoint that had not been updated yet.\n if (payload.baseStored === false) {\n enqueueSnackbar(baseMissingWarning(listing.displayName), {\n variant: 'warning',\n persist: true,\n })\n }\n /**\n * What was, and was not, checked about an email somebody else wrote.\n *\n * Said HERE because this is the moment the design enters the\n * workspace, and because the two facts are the ones a sender cannot\n * recover afterwards by looking at the design. `unreviewed` is the\n * ordinary case — email starters are auto-listed like every other\n * copied artifact — so the sentence has to be a statement rather than\n * an alarm, and it must never be silence: an install that said nothing\n * would be read as a review that happened.\n *\n * The link hosts are the inspectable half of the link policy. The\n * remote-asset rule is mechanical and the route enforces it, but where\n * a template points a tenant's own customers is a judgment only the\n * tenant can make, and they can only make it if they are shown the\n * list.\n */\n if (payload.assurance) {\n const hosts: string[] = payload.linkHosts ?? []\n enqueueSnackbar(\n (payload.assurance === 'approved'\n ? 'Reviewed by Aglyn. '\n : 'Nobody at Aglyn has reviewed this design. ') +\n (hosts.length\n ? `It links to ${hosts.join(', ')}. `\n : 'It links nowhere outside your site. ') +\n 'Read it before you send from it.',\n { variant: 'info', persist: true },\n )\n }\n // A media reference that still resolves against the publisher's own\n // library — served by our CDN, so no third party learns who opened the\n // mail, but the picture behind it is still theirs to change.\n if (payload.foreignMediaScopes?.length) {\n enqueueSnackbar(\n 'Some images in this email are still served from the publisher’s ' +\n 'own media library, so they can change after you install. ' +\n 'Replace them with your own to pin them.',\n { variant: 'info', persist: false },\n )\n }\n // A schema whose reference fields couldn't be relinked installs with\n // those fields degraded to text — silently changing a field's type\n // would be the kind of thing you discover much later (AGL-657).\n if (payload.degradedFieldIds?.length) {\n enqueueSnackbar(\n `${payload.degradedFieldIds.length} reference field(s) became ` +\n 'plain text — the datasets they pointed at are not in this ' +\n 'organization.',\n { variant: 'info', persist: false },\n )\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, queueLoading, enqueueSnackbar],\n )\n\n /**\n * Fans an install-targeting plan out to the install routes (AGL-773). Each\n * step is either a single org pin or a host pin on a named site; org steps\n * resolve the org from the acting `hostId`. One summary snackbar reports the\n * whole plan rather than N toasts. Use `resolveInstallPlan` (model) to turn\n * an All-sites / Selected-sites choice into these steps.\n */\n const installPlan = useCallback(\n async (\n listing: any,\n steps: readonly InstallPlanStep[],\n options?: {\n /**\n * Re-pinning something already installed (AGL-1017). Only the wording\n * changes — the route is the same, because moving a pin forward IS\n * installing — but \"Installed on 1 site\" is the wrong sentence to read\n * after pressing Update.\n */\n intent?: 'install' | 'update'\n /** The version being moved to, for the update summary. */\n version?: string | number | null\n },\n ) => {\n if (!steps.length) return\n const dequeue = queueLoading()\n try {\n const artifactType = listingArtifactType(listing)\n const endpoint = marketplaceInstallEndpoint(artifactType)\n let installed = 0\n const errors: string[] = []\n /**\n * Surfaced separately from success (AGL-1017/429): the pin is written\n * either way — it is an explicit choice — but a bundle built for a\n * different host ABI will not load, and a green \"Updated\" toast alone\n * would leave the workspace believing it did.\n */\n const abiWarnings = new Set<string>()\n /**\n * Sites whose copy landed with no base snapshot (AGL-2339). Counted\n * rather than toasted per step: the fan-out installs the SAME\n * artifact everywhere, so the answer is the same on every site and N\n * identical warnings would read as N different problems.\n */\n let withoutBase = 0\n /**\n * A feature lockdown refuses every step identically (AGL-1532), so\n * the fan-out stops at the first one and says \"installs are paused\"\n * ONCE. N identical refusals stacked into the error summary would\n * read as N broken sites — the opposite of what the lock is saying.\n */\n let lockedNotice: ReturnType<typeof parseLockdownRefusal> = null\n for (const step of steps) {\n // Org steps still need a host to resolve the org server-side, so\n // fall back to the acting host; host steps target their own site.\n const targetHostId = step.scope === 'host' ? step.hostId : hostId\n const response = await authorizedFetch(user, `/api/${endpoint}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n listingId: listing.$id,\n hostId: targetHostId,\n ...(step.scope === 'org' ? { scope: 'org' } : {}),\n }),\n })\n const payload = await response.json().catch(() => ({}))\n if (response.ok) {\n installed += 1\n if (payload?.hostAbiWarning) {\n abiWarnings.add(String(payload.hostAbiWarning))\n }\n if (payload?.baseStored === false) withoutBase += 1\n } else {\n lockedNotice = parseLockdownRefusal(response.status, payload)\n if (lockedNotice) break\n errors.push(payload?.error ?? 'Install failed')\n }\n }\n if (lockedNotice) {\n enqueueSnackbar(lockdownRefusalText(lockedNotice), {\n variant: 'warning',\n persist: true,\n })\n // Whatever landed before the lock DID land — saying so is the\n // difference between \"nothing happened\" and a half-applied plan\n // the workspace would otherwise discover much later.\n if (installed) {\n enqueueSnackbar(\n `${installed} site(s) were already updated before the pause.`,\n { variant: 'info', persist: false },\n )\n }\n return\n }\n const outcome = installPlanOutcome({\n name: listing.displayName,\n installed,\n errors,\n orgWide: steps.some((step) => step.scope === 'org'),\n updating: options?.intent === 'update',\n version: options?.version != null ? String(options.version) : null,\n saved: Boolean(marketplaceLandingMessage(artifactType, '')),\n })\n enqueueSnackbar(\n outcome.text,\n outcome.tone === 'success'\n ? { variant: 'success', persist: false }\n : { variant: outcome.tone, allowDuplicate: true },\n )\n for (const warning of abiWarnings) {\n enqueueSnackbar(warning, { variant: 'warning', allowDuplicate: true })\n }\n if (withoutBase) {\n enqueueSnackbar(baseMissingWarning(listing.displayName), {\n variant: 'warning',\n persist: true,\n })\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, queueLoading, enqueueSnackbar],\n )\n\n const buy = useCallback(\n async (listing: any) => {\n const name = String(listing.displayName ?? 'This listing')\n const { outcome, error, locked } = await pay.buy({\n listingId: String(listing.$id),\n displayName: listing.displayName,\n })\n if (outcome === 'paid') {\n return void enqueueSnackbar(`Bought \"${name}\". It is yours to install.`, {\n variant: 'success',\n persist: false,\n })\n }\n if (outcome === 'owned') {\n return void enqueueSnackbar(`This organization already owns \"${name}\".`, {\n variant: 'info',\n persist: false,\n })\n }\n if (outcome === 'pending') {\n return void enqueueSnackbar(\n `Your payment for \"${name}\" is going through. It is yours as soon as it clears.`,\n { variant: 'info', persist: false },\n )\n }\n // A lockdown is a pause to read, and it must not vanish unread (AGL-1532).\n if (error) {\n enqueueSnackbar(\n error,\n locked ? { variant: 'warning', persist: true } : { variant: 'error', allowDuplicate: true },\n )\n }\n },\n [pay.buy, enqueueSnackbar],\n )\n\n /**\n * Uninstall a plugin listing from the listing detail (AGL-881) — the same\n * `action:'uninstall'` the installed add-ons card uses, so it removes the\n * pin (and the switchboard entry when nothing else pins it) and decrements\n * activeInstalls. Only plugins have a removable pin; other artifact types\n * land in a library and have nothing to uninstall.\n */\n const uninstall = useCallback(\n async (\n listing: any,\n scope?: 'org' | 'host',\n // Which site's pin to drop (AGL-997). Without this the call always\n // targeted the acting host, so at org scope \"uninstall\" could only\n // mean \"the whole org pin\" or \"whichever site happened to be acting\"\n // — removing the plugin from ONE of several sites was unreachable.\n targetHostId?: string,\n ) => {\n const dequeue = queueLoading()\n try {\n const response = await authorizedFetch(\n user,\n '/api/marketplace/install-plugin',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n listingId: listing.$id,\n hostId: targetHostId ?? hostId,\n action: 'uninstall',\n ...(scope ? { scope } : {}),\n }),\n },\n )\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n // Uninstall rides the install-plugin route, so an installs lock\n // refuses it too (AGL-1532) — and \"Uninstall failed\" during an\n // incident is exactly the wrong sentence for someone trying to\n // remove the listing under investigation.\n const locked = parseLockdownRefusal(response.status, payload)\n if (locked) {\n return void enqueueSnackbar(lockdownRefusalText(locked), {\n variant: 'warning',\n persist: true,\n })\n }\n return void enqueueSnackbar(payload?.error ?? 'Uninstall failed', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n enqueueSnackbar(`Removed \"${listing.displayName}\"`, {\n variant: 'success',\n persist: false,\n })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, queueLoading, enqueueSnackbar],\n )\n\n return { install, installPlan, buy, uninstall, payPanel: pay.panel, buying: pay.busy }\n}\n\nexport default useMarketplaceActions\n"],"names":["lockdownRefusalText","parseLockdownRefusal","useLoading","useSnackbar","useCallback","useUser","authorizedFetch","listingArtifactType","installPlanOutcome","marketplaceInstallEndpoint","marketplaceLandingMessage","useMarketplacePay","baseMissingWarning","displayName","useMarketplaceActions","hostId","orgId","billingHref","data","user","enqueueSnackbar","queueLoading","pay","install","listing","scope","options","dequeue","payload","artifactType","endpoint","response","method","headers","body","JSON","stringify","listingId","$id","json","ok","diverged","onDiverged","locked","status","variant","persist","error","allowDuplicate","landed","updated","version","baseStored","assurance","hosts","linkHosts","length","join","foreignMediaScopes","degradedFieldIds","console","installPlan","steps","installed","errors","abiWarnings","Set","withoutBase","lockedNotice","step","targetHostId","catch","hostAbiWarning","add","String","push","outcome","name","orgWide","some","updating","intent","saved","Boolean","text","tone","warning","buy","uninstall","action","payPanel","panel","buying","busy"],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
*/
|
|
17
|
+
import { type ReactNode } from 'react';
|
|
18
|
+
/** How one purchase ended. */
|
|
19
|
+
export type MarketplacePayOutcome =
|
|
20
|
+
/** Paid, and the purchase is on record: the buyer owns it now. */
|
|
21
|
+
'paid'
|
|
22
|
+
/** The workspace owns it already: nothing was charged. */
|
|
23
|
+
| 'owned'
|
|
24
|
+
/** Stripe accepted the payment and it has not cleared yet. */
|
|
25
|
+
| 'pending'
|
|
26
|
+
/** Closed before paying, or it could not open: `error` says which. */
|
|
27
|
+
| 'stopped';
|
|
28
|
+
/** How one purchase ended, and why it could not open, if it could not. */
|
|
29
|
+
export interface MarketplacePayResult {
|
|
30
|
+
outcome: MarketplacePayOutcome;
|
|
31
|
+
error: string | null;
|
|
32
|
+
/** The refusal is a feature lockdown: a pause to read, not a failure of theirs. */
|
|
33
|
+
locked?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** The listing being bought, as a surface knows it. */
|
|
36
|
+
export interface PayListing {
|
|
37
|
+
listingId: string;
|
|
38
|
+
displayName?: string | null;
|
|
39
|
+
}
|
|
40
|
+
export interface MarketplacePay {
|
|
41
|
+
/** Opens payment for a listing. Settles when the purchase has ended, either way. */
|
|
42
|
+
buy: (listing: PayListing) => Promise<MarketplacePayResult>;
|
|
43
|
+
/** Asking what it would cost, or paying. */
|
|
44
|
+
busy: boolean;
|
|
45
|
+
/** The panel, in place under the button that asked for it, or `null`. */
|
|
46
|
+
panel: ReactNode;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Buying a marketplace listing in the console (AGL-4049): the card on file, or
|
|
50
|
+
* a card added right there, then the bank's step if the bank asks for one.
|
|
51
|
+
*
|
|
52
|
+
* Asks the door what it would charge before it shows anything, so the total,
|
|
53
|
+
* the prerequisite or the refusal is the first thing the buyer sees. Stripe
|
|
54
|
+
* loads only if a form or the bank's step actually needs it.
|
|
55
|
+
*
|
|
56
|
+
* One attempt per press of Pay: the same key goes with a double click and with
|
|
57
|
+
* a retry after a lost connection, so neither can charge twice (AGL-1697). An
|
|
58
|
+
* answer that ends the attempt retires the key, and pressing Pay again after a
|
|
59
|
+
* decline is a new attempt.
|
|
60
|
+
*
|
|
61
|
+
* @param input.billingHref the workspace's Billing page, for a prerequisite
|
|
62
|
+
* this person cannot meet themselves.
|
|
63
|
+
*/
|
|
64
|
+
export declare function useMarketplacePay(input: {
|
|
65
|
+
orgId: string | null | undefined;
|
|
66
|
+
/** Named so the dispatcher can gate the site this is bought from, when there is one. */
|
|
67
|
+
hostId?: string | null;
|
|
68
|
+
billingHref?: string | null;
|
|
69
|
+
}): MarketplacePay;
|
|
70
|
+
export default useMarketplacePay;
|