PageSourceSearch

https://haiphen.io/components/marketplace/marketplace-intake.js

js haiphen.io collected 2026-10-01 12:01:59 UTC 9,581 bytes, 237 lines download raw bytes

1/* docs/components/marketplace/marketplace-intake.js
2 * Marketplace token intake, OAuth redirect, and subscription activation.
3 * IIFE pattern — mounts into #marketplace-intake-mount.
4 */
5(function () {
6  'use strict';
7
8  window.HAIPHEN = window.HAIPHEN || {};
9
10  var PROVIDER_LABELS = {
11    gcp:   'Google Cloud Marketplace',
12    aws:   'AWS Marketplace',
13    azure: 'Azure Marketplace',
14  };
15
16  var RESOLVE_ENDPOINTS = {
17    gcp:   'https://checkout.haiphen.io/v1/marketplace/gcp/signup',
18    aws:   'https://checkout.haiphen.io/v1/marketplace/aws/resolve',
19    azure: 'https://checkout.haiphen.io/v1/marketplace/azure/resolve',
20  };
21
22  // Store marketplace tokens in memory only (never sessionStorage) to prevent XSS exfiltration
23  var _mktToken = null;
24  var _mktProvider = null;
25
26  function getStoredToken() { return _mktToken; }
27  function getStoredProvider() { return _mktProvider; }
28  function clearStored() { _mktToken = null; _mktProvider = null; }
29
30  function providerLabel(provider) {
31    return PROVIDER_LABELS[provider] || provider || 'Marketplace';
32  }
33
34  function renderConnecting(mount, provider) {
35    var label = providerLabel(provider);
36    mount.innerHTML = [
37      '<div class="mkt-intake">',
38      '  <div class="mkt-intake-icon">',
39      '    <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">',
40      '      <circle cx="24" cy="24" r="22" stroke="#5A9BD4" stroke-width="2"/>',
41      '      <path d="M16 24h16M24 16l8 8-8 8" stroke="#5A9BD4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
42      '    </svg>',
43      '  </div>',
44      '  <h2 class="mkt-intake-title">Connecting to ' + label + '</h2>',
45      '  <p class="mkt-intake-desc">Sign in to your Haiphen account to complete your marketplace subscription setup.</p>',
46      '  <button class="btn btn-primary mkt-signin-btn" id="mkt-signin-btn">Sign in to Haiphen</button>',
47      '  <p class="mkt-intake-note">You will be redirected back here automatically after signing in.</p>',
48      '</div>',
49    ].join('\n');
50
51    var btn = mount.querySelector('#mkt-signin-btn');
52    if (btn) {
53      btn.addEventListener('click', function () {
54        var returnTo = 'https://haiphen.io/marketplace?activate=1';
55        if (window.HAIPHEN && window.HAIPHEN.AuthSession && typeof window.HAIPHEN.AuthSession.loginUrl === 'function') {
56          window.location.assign(window.HAIPHEN.AuthSession.loginUrl(returnTo));
57        } else {
58          // Fallback: direct auth URL
59          window.location.assign('https://auth.haiphen.io/login?to=' + encodeURIComponent(returnTo));
60        }
61      });
62    }
63  }
64
65  function renderActivating(mount) {
66    mount.innerHTML = [
67      '<div class="mkt-intake">',
68      '  <div class="mkt-intake-spinner"></div>',
69      '  <h2 class="mkt-intake-title">Activating subscription…</h2>',
70      '  <p class="mkt-intake-desc">Please wait while we link your marketplace account.</p>',
71      '</div>',
72    ].join('\n');
73  }
74
75  function renderSuccess(mount, provider) {
76    var label = providerLabel(provider);
77    mount.innerHTML = [
78      '<div class="mkt-intake">',
79      '  <div class="mkt-intake-icon mkt-intake-icon--success">',
80      '    <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">',
81      '      <circle cx="24" cy="24" r="22" stroke="#10B981" stroke-width="2"/>',
82      '      <path d="M15 24l7 7 11-14" stroke="#10B981" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>',
83      '    </svg>',
84      '  </div>',
85      '  <h2 class="mkt-intake-title">Subscription Activated</h2>',
86      '  <p class="mkt-intake-desc">Your ' + label + ' subscription is now linked to your Haiphen account. Redirecting to your services…</p>',
87      '</div>',
88    ].join('\n');
89
90    setTimeout(function () {
91      if (typeof window.showSection === 'function') {
92        window.showSection('Services');
93        history.pushState(null, '', '/services');
94      } else {
95        window.location.assign('/#services');
96      }
97    }, 2000);
98  }
99
100  function renderError(mount, message) {
101    mount.innerHTML = [
102      '<div class="mkt-intake">',
103      '  <div class="mkt-intake-icon mkt-intake-icon--error">',
104      '    <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">',
105      '      <circle cx="24" cy="24" r="22" stroke="#EF4444" stroke-width="2"/>',
106      '      <path d="M16 16l16 16M32 16L16 32" stroke="#EF4444" stroke-width="2.5" stroke-linecap="round"/>',
107      '    </svg>',
108      '  </div>',
109      '  <h2 class="mkt-intake-title">Activation Failed</h2>',
110      '  <p class="mkt-intake-desc">' + (message || 'Something went wrong.') + '</p>',
111      '  <p class="mkt-intake-note">Need help? Contact <a href="mailto:[email protected]">[email protected]</a></p>',
112      '</div>',
113    ].join('\n');
114  }
115
116  function renderInvalid(mount) {
117    mount.innerHTML = [
118      '<div class="mkt-intake">',
119      '  <h2 class="mkt-intake-title">Invalid Link</h2>',
120      '  <p class="mkt-intake-desc">This marketplace link is invalid or has expired.</p>',
121      '  <p class="mkt-intake-note">Return to the marketplace listing and click the subscription link again,',
122      '  or contact <a href="mailto:[email protected]">[email protected]</a> for assistance.</p>',
123      '</div>',
124    ].join('\n');
125  }
126
127  async function activateSubscription(mount, provider, token) {
128    renderActivating(mount);
129
130    var endpoint = RESOLVE_ENDPOINTS[provider];
131    if (!endpoint) {
132      renderError(mount, 'Unknown marketplace provider: ' + provider);
133      return;
134    }
135
136    try {
137      var resp = await fetch(endpoint, {
138        method: 'POST',
139        headers: { 'Content-Type': 'application/json' },
140        credentials: 'include',
141        body: JSON.stringify({ token: token }),
142      });
143
144      var data = null;
145      try { data = await resp.json(); } catch(e) {}
146
147      if (!resp.ok || (data && data.ok === false)) {
148        var errMsg = (data && (data.error || data.message)) || ('HTTP ' + resp.status);
149        renderError(mount, 'Activation error: ' + errMsg);
150        clearStored();
151        return;
152      }
153
154      clearStored();
155      renderSuccess(mount, provider);
156    } catch (err) {
157      renderError(mount, 'Network error. Please check your connection and try again.');
158    }
159  }
160
161  function injectCss() {
162    if (document.querySelector('style[data-haiphen="mkt-intake-css"]')) return;
163    var style = document.createElement('style');
164    style.setAttribute('data-haiphen', 'mkt-intake-css');
165    style.textContent = [
166      '.mkt-intake { max-width: 480px; margin: 80px auto; padding: 40px 32px; text-align: center;',
167      '  background: var(--surface, #1a1f2e); border: 1px solid var(--border, #2d3748);',
168      '  border-radius: 12px; }',
169      '.mkt-intake-icon { margin-bottom: 24px; }',
170      '.mkt-intake-icon--success svg circle { stroke: #10B981; }',
171      '.mkt-intake-icon--error svg circle { stroke: #EF4444; }',
172      '.mkt-intake-title { font-size: 1.5rem; font-weight: 600; margin: 0 0 12px;',
173      '  color: var(--text-primary, #e2e8f0); }',
174      '.mkt-intake-desc { color: var(--text-secondary, #94a3b8); margin: 0 0 24px; line-height: 1.6; }',
175      '.mkt-intake-note { font-size: 0.85rem; color: var(--text-muted, #64748b); margin: 16px 0 0; }',
176      '.mkt-intake-note a { color: var(--accent, #5A9BD4); text-decoration: none; }',
177      '.mkt-intake-note a:hover { text-decoration: underline; }',
178      '.mkt-intake-spinner { width: 40px; height: 40px; border: 3px solid var(--border, #2d3748);',
179      '  border-top-color: #5A9BD4; border-radius: 50%; animation: mkt-spin 0.8s linear infinite;',
180      '  margin: 0 auto 24px; }',
181      '@keyframes mkt-spin { to { transform: rotate(360deg); } }',
182      '.mkt-signin-btn { margin-top: 8px; }',
183    ].join(' ');
184    document.head.appendChild(style);
185  }
186
187  function mount() {
188    var mountEl = document.getElementById('marketplace-intake-mount');
189    if (!mountEl) return;
190
191    injectCss();
192
193    var qs = new URLSearchParams(window.location.search);
194    var activate = qs.get('activate');
195    var provider = getStoredProvider();
196    var token = getStoredToken();
197
198    // Capture provider + token from URL query params (fulfillment redirect)
199    var urlProvider = qs.get('provider');
200    var urlToken = qs.get('token');
201    if (urlProvider && urlToken) {
202      _mktToken = urlToken;
203      _mktProvider = urlProvider;
204      provider = urlProvider;
205      token = urlToken;
206    }
207
208    if (activate === '1' && provider && token) {
209      // User has just returned from OAuth — activate the subscription
210      activateSubscription(mountEl, provider, token);
211      return;
212    }
213
214    if (token && provider) {
215      // Token present in sessionStorage but no activate flag — show sign-in prompt
216      renderConnecting(mountEl, provider);
217      return;
218    }
219
220    // No token, no activate — link is invalid
221    renderInvalid(mountEl);
222  }
223
224  window.HAIPHEN.MarketplaceIntake = { mount: mount };
225
226  // Auto-mount when section is revealed (showSection sets innerHTML then calls mount)
227  document.addEventListener('DOMContentLoaded', function () {
228    // If the page was loaded directly on /marketplace, mount immediately
229    if (window.location.pathname === '/marketplace' ||
230        new URLSearchParams(window.location.search).get('activate') === '1' ||
231        new URLSearchParams(window.location.search).get('token')) {
232      // routeFromUrl will trigger — just ensure mount is called after section renders
233      setTimeout(mount, 50);
234    }
235  });
236
237})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.