@blocklet/payment-react-headless 1.29.8 → 1.29.9

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.
@@ -49,6 +49,7 @@ export function useCustomerForm(sessionData, currencyId, methodId) {
49
49
  }));
50
50
  }
51
51
  } catch {
52
+ setPrefetched(true);
52
53
  }
53
54
  };
54
55
  fetchCustomer();
@@ -105,6 +106,7 @@ export function useCustomerForm(sessionData, currencyId, methodId) {
105
106
  }));
106
107
  }
107
108
  } catch {
109
+ setPrefetched(true);
108
110
  }
109
111
  });
110
112
  return {
@@ -105,6 +105,29 @@ export function useSubmit(sessionData, sessionId, currencyId, isStripe, isCredit
105
105
  setContext({ type: "error", message: getErrorMessage(err) || "Payment verification failed" });
106
106
  }
107
107
  });
108
+ useEffect(() => {
109
+ if (completedDetectedRef.current || !sessionId) return;
110
+ if (status !== "idle" && status !== "waiting_stripe") return;
111
+ let redirectStatus = null;
112
+ let hasPaymentIntent = false;
113
+ try {
114
+ const params = new URLSearchParams(window.location.search);
115
+ redirectStatus = params.get("redirect_status");
116
+ hasPaymentIntent = !!params.get("payment_intent");
117
+ } catch {
118
+ return;
119
+ }
120
+ if (!redirectStatus && !hasPaymentIntent) return;
121
+ completedDetectedRef.current = true;
122
+ if (redirectStatus === "failed" || redirectStatus === "canceled") {
123
+ setStatus("failed");
124
+ setContext({ type: "error", message: "Payment was not completed. Please try again." });
125
+ return;
126
+ }
127
+ setStatus("submitting");
128
+ setContext(null);
129
+ handleCompletion();
130
+ }, [sessionId]);
108
131
  useEffect(() => {
109
132
  if (status !== "completed" || !sessionId) return void 0;
110
133
  const hasVendors = session?.line_items?.some(
@@ -62,7 +62,9 @@ function useCustomerForm(sessionData, currencyId, methodId) {
62
62
  }
63
63
  }));
64
64
  }
65
- } catch {}
65
+ } catch {
66
+ setPrefetched(true);
67
+ }
66
68
  };
67
69
  fetchCustomer();
68
70
  }, [session?.id]);
@@ -124,7 +126,9 @@ function useCustomerForm(sessionData, currencyId, methodId) {
124
126
  }
125
127
  }));
126
128
  }
127
- } catch {}
129
+ } catch {
130
+ setPrefetched(true);
131
+ }
128
132
  });
129
133
  return {
130
134
  fields,
@@ -104,6 +104,32 @@ function useSubmit(sessionData, sessionId, currencyId, isStripe, isCredit, isDon
104
104
  });
105
105
  }
106
106
  });
107
+ (0, _react.useEffect)(() => {
108
+ if (completedDetectedRef.current || !sessionId) return;
109
+ if (status !== "idle" && status !== "waiting_stripe") return;
110
+ let redirectStatus = null;
111
+ let hasPaymentIntent = false;
112
+ try {
113
+ const params = new URLSearchParams(window.location.search);
114
+ redirectStatus = params.get("redirect_status");
115
+ hasPaymentIntent = !!params.get("payment_intent");
116
+ } catch {
117
+ return;
118
+ }
119
+ if (!redirectStatus && !hasPaymentIntent) return;
120
+ completedDetectedRef.current = true;
121
+ if (redirectStatus === "failed" || redirectStatus === "canceled") {
122
+ setStatus("failed");
123
+ setContext({
124
+ type: "error",
125
+ message: "Payment was not completed. Please try again."
126
+ });
127
+ return;
128
+ }
129
+ setStatus("submitting");
130
+ setContext(null);
131
+ handleCompletion();
132
+ }, [sessionId]);
107
133
  (0, _react.useEffect)(() => {
108
134
  if (status !== "completed" || !sessionId) return void 0;
109
135
  const hasVendors = session?.line_items?.some(item => !!item?.price?.product?.vendor_config?.length);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/payment-react-headless",
3
- "version": "1.29.8",
3
+ "version": "1.29.9",
4
4
  "description": "Headless React hooks for payment-kit checkout",
5
5
  "keywords": [
6
6
  "react",
@@ -35,7 +35,7 @@
35
35
  "dependencies": {
36
36
  "@arcblock/ws": "^1.30.24",
37
37
  "@blocklet/js-sdk": "workspace:*",
38
- "@blocklet/payment-types": "1.29.8",
38
+ "@blocklet/payment-types": "1.29.9",
39
39
  "@ocap/util": "^1.30.24",
40
40
  "ahooks": "^3.8.5",
41
41
  "google-libphonenumber": "^3.2.42",
@@ -60,5 +60,5 @@
60
60
  "publishConfig": {
61
61
  "access": "public"
62
62
  },
63
- "gitHead": "547c0614f1f83abad80ea08452d2f5a9f4f5aa07"
63
+ "gitHead": "249bd9bf3c6cc54f77c260edb4b4d3c0be60f126"
64
64
  }
@@ -82,7 +82,11 @@ export function useCustomerForm(
82
82
  }));
83
83
  }
84
84
  } catch {
85
- // Ignore - customer info is optional
85
+ // /customers/me is best-effort. Still mark prefetch as attempted so the
86
+ // customer-info card renders an editable form (letting the user fill the
87
+ // required name/email manually) instead of staying hidden forever — e.g.
88
+ // when the endpoint 403s under embedded (no resolvable payment session).
89
+ setPrefetched(true);
86
90
  }
87
91
  };
88
92
 
@@ -149,7 +153,8 @@ export function useCustomerForm(
149
153
  }));
150
154
  }
151
155
  } catch {
152
- // Ignore - customer info is optional
156
+ // Best-effort (see prefetch above) — mark attempted so the form still renders.
157
+ setPrefetched(true);
153
158
  }
154
159
  });
155
160
 
@@ -197,6 +197,48 @@ export function useSubmit(
197
197
  }
198
198
  });
199
199
 
200
+ // Stripe redirect fallback: when confirmPayment redirects back to return_url
201
+ // (3DS or redirect-based methods), the page fully reloads — onConfirm →
202
+ // stripeConfirm never runs, and the completion event only arrives via the
203
+ // WebSocket relay, which is unreachable on embedded/no-relay runtimes. Stripe
204
+ // appends redirect_status / payment_intent to the return URL; detect it and
205
+ // start polling until the backend reconciles, instead of getting stuck on
206
+ // loading until the user manually refreshes.
207
+ useEffect(() => {
208
+ if (completedDetectedRef.current || !sessionId) return;
209
+ if (status !== 'idle' && status !== 'waiting_stripe') return;
210
+ let redirectStatus: string | null = null;
211
+ let hasPaymentIntent = false;
212
+ try {
213
+ const params = new URLSearchParams(window.location.search);
214
+ redirectStatus = params.get('redirect_status');
215
+ hasPaymentIntent = !!params.get('payment_intent');
216
+ } catch {
217
+ return;
218
+ }
219
+ // Not a Stripe redirect return — nothing to reconcile.
220
+ if (!redirectStatus && !hasPaymentIntent) return;
221
+
222
+ completedDetectedRef.current = true;
223
+
224
+ // A failed/canceled redirect ALSO carries `payment_intent`, so the bare presence
225
+ // of that param is not a success signal. Surface the failure immediately instead
226
+ // of polling a session that will never reach paid until the 5-minute timeout.
227
+ if (redirectStatus === 'failed' || redirectStatus === 'canceled') {
228
+ setStatus('failed');
229
+ setContext({ type: 'error', message: 'Payment was not completed. Please try again.' });
230
+ return;
231
+ }
232
+
233
+ // succeeded / pending / (param present without an explicit status) → the backend
234
+ // is the source of truth; poll until it reconciles. waitForCheckoutComplete also
235
+ // early-throws on a surfaced last_payment_error, so a hard decline still ends fast.
236
+ setStatus('submitting');
237
+ setContext(null);
238
+ handleCompletion();
239
+ // eslint-disable-next-line react-hooks/exhaustive-deps
240
+ }, [sessionId]);
241
+
200
242
  // Vendor order polling after completion
201
243
  useEffect(() => {
202
244
  if (status !== 'completed' || !sessionId) return undefined;