@growth-operations/ui-shared 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growth-operations/ui-shared",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Shared HubSpot UI-extension modules (Home, Billing) for Growth Operations apps.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -145,6 +145,11 @@ function usePortalSession({ context, state, appKey }) {
145
145
  const [loading, setLoading] = useState(true);
146
146
  const [error, setError] = useState(null);
147
147
  const [portalUrl, setPortalUrl] = useState(null);
148
+ // "Not ready" = the install hasn't been provisioned with a Stripe customer
149
+ // yet (fresh/old account: no entitlement.status, or the portal endpoint 409s
150
+ // with "no Stripe customer"). That's an expected pre-purchase state, not an
151
+ // error — the UI shows a calm "finish setup" message instead of a red alert.
152
+ const [notReady, setNotReady] = useState(false);
148
153
 
149
154
  // Pre-create on mount so the link is one real click (Stripe can't be iframed;
150
155
  // UI Extensions have no open-URL action — only Link, whose href must exist
@@ -162,6 +167,17 @@ function usePortalSession({ context, state, appKey }) {
162
167
  resetOnUnmount();
163
168
  return;
164
169
  }
170
+ // No Stripe customer yet → don't even try to open a portal. An install
171
+ // that hasn't been provisioned (no entitlement, or status null/
172
+ // not_installed) has nothing to manage; show the calm "not ready" state.
173
+ const status = state?.entitlement?.status;
174
+ if (!state.entitlement || status == null || status === "not_installed") {
175
+ if (mounted.current) {
176
+ setNotReady(true);
177
+ setLoading(false);
178
+ }
179
+ return;
180
+ }
165
181
  try {
166
182
  const base = state.billing_base_url;
167
183
  if (!base) throw new Error("Billing service not configured");
@@ -187,25 +203,54 @@ function usePortalSession({ context, state, appKey }) {
187
203
  });
188
204
  if (!res.ok) {
189
205
  const text = await res.text();
190
- throw new Error(`Billing portal request failed (${res.status}): ${text}`);
206
+ // 409 "no Stripe customer yet" is the not-provisioned case, not a
207
+ // failure — surface it as the calm not-ready state.
208
+ if (res.status === 409 && /no stripe customer/i.test(text)) {
209
+ if (mounted.current) setNotReady(true);
210
+ } else {
211
+ throw new Error(`Billing portal request failed (${res.status}): ${text}`);
212
+ }
213
+ } else {
214
+ const { url } = await res.json();
215
+ if (!url) throw new Error("No billing portal URL returned");
216
+ if (mounted.current) setPortalUrl(url);
191
217
  }
192
- const { url } = await res.json();
193
- if (!url) throw new Error("No billing portal URL returned");
194
- if (mounted.current) setPortalUrl(url);
195
218
  } catch (perr) {
196
219
  if (mounted.current) setError(String(perr));
197
220
  } finally {
198
221
  if (mounted.current) setLoading(false);
199
222
  }
200
223
  },
201
- [context, appKey, state?.billing_base_url, state?.app_id]
224
+ [context, appKey, state?.billing_base_url, state?.app_id, state?.entitlement?.status]
202
225
  );
203
226
 
204
- return { portalUrl, loading, error };
227
+ return { portalUrl, loading, error, notReady };
205
228
  }
206
229
 
207
230
  function TrialSubscriptionBilling({ context, state, appKey }) {
208
- const { portalUrl, loading, error } = usePortalSession({ context, state, appKey });
231
+ const { portalUrl, loading, error, notReady } = usePortalSession({
232
+ context,
233
+ state,
234
+ appKey,
235
+ });
236
+
237
+ // Install not provisioned with a Stripe customer yet — calm "finish setup"
238
+ // state, not a red error. (Old/stale installs, or before the install flow's
239
+ // provision_trial_install has run.)
240
+ if (notReady) {
241
+ return (
242
+ <Flex direction="column" gap="medium">
243
+ <Alert title="Billing isn't set up yet" variant="info">
244
+ <Text>
245
+ Your trial and billing are still being set up for this install. Once
246
+ setup completes, manage your plan and payment method here. If this
247
+ persists, reinstall the app from the HubSpot marketplace to complete
248
+ sign-in.
249
+ </Text>
250
+ </Alert>
251
+ </Flex>
252
+ );
253
+ }
209
254
 
210
255
  return (
211
256
  <Flex direction="column" gap="medium">