@mission_sciences/provider-sdk 0.3.0 → 0.4.0-dev.f853e1d

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.
@@ -20,6 +20,19 @@ class SDKError extends Error {
20
20
  }
21
21
  }
22
22
  }
23
+ class PurchaseError extends SDKError {
24
+ constructor(message2, code, itemId, statusCode) {
25
+ super(message2, code, statusCode);
26
+ this.itemId = itemId;
27
+ this.name = "PurchaseError";
28
+ }
29
+ }
30
+ var PurchaseState = /* @__PURE__ */ ((PurchaseState2) => {
31
+ PurchaseState2["AVAILABLE"] = "AVAILABLE";
32
+ PurchaseState2["PRIVY_REQUIRED"] = "PRIVY_REQUIRED";
33
+ PurchaseState2["INSUFFICIENT_FUNDS"] = "INSUFFICIENT_FUNDS";
34
+ return PurchaseState2;
35
+ })(PurchaseState || {});
23
36
  class JWTParser {
24
37
  /**
25
38
  * Decode JWT payload without verification
@@ -2017,6 +2030,55 @@ class TabSyncManager {
2017
2030
  this.logger.log("Tab sync destroyed");
2018
2031
  }
2019
2032
  }
2033
+ class PurchaseStateManager {
2034
+ constructor(config) {
2035
+ this.config = {
2036
+ timeout: 5e3,
2037
+ ...config
2038
+ };
2039
+ }
2040
+ /**
2041
+ * Check purchase state for a specific item
2042
+ * Filters raw API response to expose only safe data to publisher apps
2043
+ */
2044
+ async checkItemPurchaseState(itemId) {
2045
+ try {
2046
+ const response = await fetch(`${this.config.apiEndpoint}/items/${itemId}/purchase-state`, {
2047
+ method: "GET",
2048
+ headers: {
2049
+ "Content-Type": "application/json"
2050
+ },
2051
+ signal: AbortSignal.timeout(this.config.timeout)
2052
+ });
2053
+ if (!response.ok) {
2054
+ throw new Error(`API request failed: ${response.status} ${response.statusText}`);
2055
+ }
2056
+ const rawData = await response.json();
2057
+ return this.filterPurchaseStateResponse(rawData);
2058
+ } catch (error) {
2059
+ if (error instanceof Error) {
2060
+ throw error;
2061
+ }
2062
+ throw new Error("Unknown error occurred while checking purchase state");
2063
+ }
2064
+ }
2065
+ /**
2066
+ * Filters raw API response to expose only safe data to publisher apps
2067
+ * CRITICAL: Raw privyEligibility map must NEVER be exposed to apps
2068
+ */
2069
+ filterPurchaseStateResponse(rawResponse) {
2070
+ const filteredResponse = {
2071
+ state: rawResponse.state
2072
+ };
2073
+ if (rawResponse.state === PurchaseState.PRIVY_REQUIRED && rawResponse.privyEligibility) {
2074
+ const requiredLevels = Object.values(rawResponse.privyEligibility).filter((details) => details.requiresUpgrade).map((details) => details.requiredLevel);
2075
+ if (requiredLevels.length > 0) {
2076
+ filteredResponse.requiredLevel = Math.max(...requiredLevels);
2077
+ }
2078
+ }
2079
+ return filteredResponse;
2080
+ }
2081
+ }
2020
2082
  const lightTheme = {
2021
2083
  colors: {
2022
2084
  // Background
@@ -2503,6 +2565,166 @@ function extractTokenFromURL(paramName = "gwSession", url) {
2503
2565
  function isBrowser() {
2504
2566
  return typeof window !== "undefined" && typeof window.document !== "undefined";
2505
2567
  }
2568
+ class PurchaseModal {
2569
+ constructor(themeMode = "light", customStyles) {
2570
+ this.modal = null;
2571
+ this.legacyStyles = null;
2572
+ const prefersDark = themeMode === "dark" || themeMode === "auto" && this.detectDarkMode();
2573
+ this.theme = getTheme(prefersDark);
2574
+ if (customStyles) {
2575
+ this.legacyStyles = {
2576
+ backgroundColor: customStyles.backgroundColor || "#ffffff",
2577
+ textColor: customStyles.textColor || "#333333",
2578
+ primaryColor: customStyles.primaryColor || "#007bff",
2579
+ borderRadius: customStyles.borderRadius || "8px",
2580
+ fontFamily: customStyles.fontFamily || '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
2581
+ };
2582
+ }
2583
+ }
2584
+ detectDarkMode() {
2585
+ if (typeof window !== "undefined" && window.matchMedia) {
2586
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
2587
+ }
2588
+ return false;
2589
+ }
2590
+ escapeHtml(text) {
2591
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2592
+ }
2593
+ /**
2594
+ * Show purchase confirmation modal
2595
+ */
2596
+ show(options) {
2597
+ this.hide();
2598
+ this.modal = document.createElement("div");
2599
+ this.modal.id = "gw-purchase-modal";
2600
+ this.modal.style.cssText = `
2601
+ position: fixed;
2602
+ top: 0;
2603
+ left: 0;
2604
+ width: 100%;
2605
+ height: 100%;
2606
+ background-color: rgba(0, 0, 0, 0.5);
2607
+ display: flex;
2608
+ align-items: center;
2609
+ justify-content: center;
2610
+ z-index: 99999;
2611
+ font-family: ${this.legacyStyles?.fontFamily || this.theme.typography.fontFamily};
2612
+ `;
2613
+ const content = document.createElement("div");
2614
+ const bgColor = this.legacyStyles?.backgroundColor || this.theme.colors.card;
2615
+ const textColor = this.legacyStyles?.textColor || this.theme.colors.cardForeground;
2616
+ const borderRadius = this.legacyStyles?.borderRadius || this.theme.spacing.borderRadius.lg;
2617
+ content.style.cssText = `
2618
+ background-color: ${bgColor};
2619
+ color: ${textColor};
2620
+ border-radius: ${borderRadius};
2621
+ padding: ${this.theme.spacing.padding.lg};
2622
+ max-width: 400px;
2623
+ width: 90%;
2624
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
2625
+ border: 1px solid ${this.theme.colors.border};
2626
+ `;
2627
+ content.innerHTML = `
2628
+ <h2 style="margin: 0 0 ${this.theme.spacing.padding.md} 0; font-size: ${this.theme.typography.fontSize.xl}; font-weight: ${this.theme.typography.fontWeight.semibold}; color: ${textColor};">
2629
+ 🛒 Confirm Purchase
2630
+ </h2>
2631
+ <p style="margin: 0 0 ${this.theme.spacing.padding.lg} 0; font-size: ${this.theme.typography.fontSize.base}; line-height: ${this.theme.typography.lineHeight.normal}; color: ${this.theme.colors.mutedForeground};">
2632
+ Are you sure you want to purchase item <strong style="color: ${textColor};">${this.escapeHtml(options.itemId)}</strong>?
2633
+ </p>
2634
+ <div style="display: flex; gap: ${this.theme.spacing.gap.md}; justify-content: flex-end;">
2635
+ <button id="gw-cancel-btn" style="
2636
+ padding: 10px 20px;
2637
+ background-color: ${this.theme.colors.secondary};
2638
+ color: ${this.theme.colors.secondaryForeground};
2639
+ border: 1px solid ${this.theme.colors.border};
2640
+ border-radius: ${this.theme.spacing.borderRadius.sm};
2641
+ font-size: ${this.theme.typography.fontSize.sm};
2642
+ font-weight: ${this.theme.typography.fontWeight.medium};
2643
+ cursor: pointer;
2644
+ transition: all 0.2s;
2645
+ font-family: ${this.theme.typography.fontFamily};
2646
+ ">
2647
+ Cancel
2648
+ </button>
2649
+ <button id="gw-confirm-btn" style="
2650
+ padding: 10px 20px;
2651
+ background-color: ${this.theme.colors.primary};
2652
+ color: ${this.theme.colors.primaryForeground};
2653
+ border: none;
2654
+ border-radius: ${this.theme.spacing.borderRadius.sm};
2655
+ font-size: ${this.theme.typography.fontSize.sm};
2656
+ font-weight: ${this.theme.typography.fontWeight.medium};
2657
+ cursor: pointer;
2658
+ transition: all 0.2s;
2659
+ font-family: ${this.theme.typography.fontFamily};
2660
+ ">
2661
+ Confirm Purchase
2662
+ </button>
2663
+ </div>
2664
+ `;
2665
+ this.modal.appendChild(content);
2666
+ document.body.appendChild(this.modal);
2667
+ const confirmBtn = document.getElementById("gw-confirm-btn");
2668
+ if (confirmBtn && options.onConfirm) {
2669
+ confirmBtn.addEventListener("click", () => {
2670
+ options.onConfirm?.();
2671
+ this.hide();
2672
+ });
2673
+ }
2674
+ const cancelBtn = document.getElementById("gw-cancel-btn");
2675
+ if (cancelBtn) {
2676
+ cancelBtn.addEventListener("click", () => {
2677
+ options.onCancel?.();
2678
+ this.hide();
2679
+ });
2680
+ }
2681
+ const buttons = content.querySelectorAll("button");
2682
+ buttons.forEach((button) => {
2683
+ button.addEventListener("mouseenter", () => {
2684
+ button.style.opacity = "0.9";
2685
+ });
2686
+ button.addEventListener("mouseleave", () => {
2687
+ button.style.opacity = "1";
2688
+ });
2689
+ button.addEventListener("focus", () => {
2690
+ button.style.outline = `2px solid ${this.theme.colors.ring}`;
2691
+ button.style.outlineOffset = "2px";
2692
+ });
2693
+ button.addEventListener("blur", () => {
2694
+ button.style.outline = "none";
2695
+ });
2696
+ });
2697
+ const handleKeyDown = (e) => {
2698
+ if (e.key === "Escape") {
2699
+ options.onCancel?.();
2700
+ this.hide();
2701
+ document.removeEventListener("keydown", handleKeyDown);
2702
+ }
2703
+ };
2704
+ document.addEventListener("keydown", handleKeyDown);
2705
+ this.modal.addEventListener("click", (e) => {
2706
+ if (e.target === this.modal) {
2707
+ options.onCancel?.();
2708
+ this.hide();
2709
+ }
2710
+ });
2711
+ }
2712
+ /**
2713
+ * Hide and remove modal
2714
+ */
2715
+ hide() {
2716
+ if (this.modal && this.modal.parentNode) {
2717
+ this.modal.parentNode.removeChild(this.modal);
2718
+ this.modal = null;
2719
+ }
2720
+ }
2721
+ /**
2722
+ * Check if modal is currently shown
2723
+ */
2724
+ isShown() {
2725
+ return this.modal !== null;
2726
+ }
2727
+ }
2506
2728
  class MarketplaceSDK {
2507
2729
  constructor(config) {
2508
2730
  this.timer = null;
@@ -2513,8 +2735,21 @@ class MarketplaceSDK {
2513
2735
  this.sessionData = null;
2514
2736
  this.jwtToken = null;
2515
2737
  this.endReason = "manual";
2738
+ this.purchaseModal = null;
2739
+ if (!config.jwksUri) {
2740
+ throw new SDKError(
2741
+ "jwksUri is required — pass an environment-aware JWKS URL (e.g., https://api.dev.generalwisdom.com/.well-known/jwks.json). See README for environment-specific URLs.",
2742
+ "CONFIG_MISSING_JWKS_URI"
2743
+ );
2744
+ }
2745
+ if (!config.marketplaceUrl) {
2746
+ throw new SDKError(
2747
+ "marketplaceUrl is required — pass an environment-aware marketplace URL (e.g., https://dev.generalwisdom.com/). See README for environment-specific URLs.",
2748
+ "CONFIG_MISSING_MARKETPLACE_URL"
2749
+ );
2750
+ }
2516
2751
  this.config = {
2517
- jwksUri: config.jwksUri || "https://api.platform.generalwisdom.com/.well-known/jwks.json",
2752
+ jwksUri: config.jwksUri,
2518
2753
  jwtParamName: config.jwtParamName || "gwSession",
2519
2754
  apiEndpoint: config.apiEndpoint || "https://api.platform.generalwisdom.com",
2520
2755
  jwtIssuer: config.jwtIssuer || "generalwisdom.com",
@@ -2524,7 +2759,7 @@ class MarketplaceSDK {
2524
2759
  customStyles: config.customStyles ?? {},
2525
2760
  themeMode: config.themeMode ?? "light",
2526
2761
  applicationId: config.applicationId ?? "",
2527
- marketplaceUrl: config.marketplaceUrl ?? "https://platform.generalwisdom.com/",
2762
+ marketplaceUrl: config.marketplaceUrl,
2528
2763
  // Phase 2 options
2529
2764
  enableHeartbeat: config.enableHeartbeat ?? false,
2530
2765
  heartbeatIntervalSeconds: config.heartbeatIntervalSeconds ?? 30,
@@ -2537,6 +2772,9 @@ class MarketplaceSDK {
2537
2772
  };
2538
2773
  this.validator = new JWKSValidator(this.config.jwksUri, this.config.debug);
2539
2774
  this.logger = new Logger(this.config.debug, "[MarketplaceSDK]");
2775
+ this.purchaseStateManager = new PurchaseStateManager({
2776
+ apiEndpoint: this.config.apiEndpoint
2777
+ });
2540
2778
  this.logger.info("SDK initialized with config:", {
2541
2779
  jwksUri: this.config.jwksUri,
2542
2780
  jwtParamName: this.config.jwtParamName,
@@ -3046,6 +3284,79 @@ class MarketplaceSDK {
3046
3284
  isTimerRunning() {
3047
3285
  return this.timer?.isRunning() ?? false;
3048
3286
  }
3287
+ /**
3288
+ * Request purchase of an item — opens PurchaseModal directly
3289
+ * Bypasses the add-ons panel; works for any active item (visible or hidden)
3290
+ */
3291
+ requestPurchase(itemId) {
3292
+ if (!this.sessionData || !this.jwtToken) {
3293
+ throw new SDKError("No active session", "NO_SESSION");
3294
+ }
3295
+ this.logger.info("Requesting purchase for item:", itemId);
3296
+ this.events.onPurchaseStart?.({ itemId, quantity: 1 });
3297
+ if (!this.purchaseModal) {
3298
+ this.purchaseModal = new PurchaseModal(
3299
+ this.config.themeMode || "light",
3300
+ this.config.customStyles
3301
+ );
3302
+ }
3303
+ this.purchaseModal.show({
3304
+ itemId,
3305
+ onConfirm: async () => {
3306
+ try {
3307
+ const response = await fetch(
3308
+ `${this.config.apiEndpoint}/items/${itemId}/purchase`,
3309
+ {
3310
+ method: "POST",
3311
+ headers: {
3312
+ "Authorization": `Bearer ${this.jwtToken}`,
3313
+ "Content-Type": "application/json"
3314
+ },
3315
+ body: JSON.stringify({ item_id: itemId, quantity: 1 })
3316
+ }
3317
+ );
3318
+ if (!response.ok) {
3319
+ throw new PurchaseError(
3320
+ "Purchase failed",
3321
+ "PURCHASE_FAILED",
3322
+ itemId,
3323
+ response.status
3324
+ );
3325
+ }
3326
+ const data = await response.json();
3327
+ const result = {
3328
+ itemId,
3329
+ transactionId: data.transactionId,
3330
+ amount: data.amount
3331
+ };
3332
+ this.events.onPurchaseSuccess?.(result);
3333
+ this.events.onPurchaseComplete?.(itemId);
3334
+ if (data.newBalance !== void 0) {
3335
+ this.events.onBalanceUpdate?.(data.newBalance);
3336
+ }
3337
+ this.logger.info("Purchase successful:", result);
3338
+ } catch (error) {
3339
+ const purchaseError = error instanceof PurchaseError ? error : new PurchaseError(
3340
+ error instanceof Error ? error.message : "Purchase failed",
3341
+ "PURCHASE_ERROR",
3342
+ itemId
3343
+ );
3344
+ this.events.onPurchaseError?.(purchaseError);
3345
+ this.logger.error("Purchase failed:", purchaseError);
3346
+ }
3347
+ },
3348
+ onCancel: () => {
3349
+ this.events.onPurchaseCancelled?.(itemId);
3350
+ this.logger.info("Purchase cancelled for item:", itemId);
3351
+ }
3352
+ });
3353
+ }
3354
+ /**
3355
+ * Get purchase state manager for item purchase state checking
3356
+ */
3357
+ getPurchaseStateManager() {
3358
+ return this.purchaseStateManager;
3359
+ }
3049
3360
  /**
3050
3361
  * Cleanup and destroy SDK instance
3051
3362
  */
@@ -3055,6 +3366,7 @@ class MarketplaceSDK {
3055
3366
  this.heartbeat?.stop();
3056
3367
  this.tabSync?.destroy();
3057
3368
  this.modal?.hide();
3369
+ this.purchaseModal?.hide();
3058
3370
  if (typeof sessionStorage !== "undefined") {
3059
3371
  sessionStorage.removeItem("gw_marketplace_jwt");
3060
3372
  this.logger.log("JWT token cleared from storage");
@@ -3265,6 +3577,8 @@ export {
3265
3577
  JWTParser,
3266
3578
  Logger,
3267
3579
  MarketplaceSDK,
3580
+ PurchaseError,
3581
+ PurchaseModal,
3268
3582
  SDKError,
3269
3583
  SessionHeader,
3270
3584
  TabSyncManager,