@pollar/react 0.11.4-rc.4 → 0.11.4

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  React bindings for [Pollar](https://pollar.xyz) — drop-in authentication UI, transaction modals, and hooks for
4
4
  Stellar and Solana applications.
5
5
 
6
- > **0.11.4** (candidate `0.11.4-rc.1` on the `next` tag, which requires `@pollar/core@^0.11.4-rc.1`).
6
+ > **0.11.4** (requires `@pollar/core@^0.11.4`).
7
7
  > **KYC by ramp corridor**: `<RampWidget>` lists routes locked by a requirement step and opens that
8
8
  > step - `<KycModal>`, `<RequirementFormModal>`, `<RegistryCheckModal>` or `<ProviderRegistrationModal>`
9
9
  > (all exported) - then quotes again; `openKycModal()` without a corridor walks the app's own KYC
@@ -32,6 +32,14 @@ Stellar and Solana applications.
32
32
  > pointless KYC poll), and three failures that printed as raw error codes get sentences.
33
33
  > `RampWidgetTemplateProps` gains an optional `onboardingStatus`.
34
34
  >
35
+ > **Cards**: `<CardModal>`, opened with `openCardModal()` from `usePollar()`, walks the user through
36
+ > the card provider's steps, sign-up and issuing, then shows the card with the app's logo and card
37
+ > label, its balance and movements, funding from the wallet (each funding's step while it is in
38
+ > flight) and the number revealed on the device. **Wallets on demand**: `usePollar()` exposes
39
+ > `hasWallet` and `createWallet`, and signed in without a wallet `<WalletButton>` offers Create wallet
40
+ > to an app that creates wallets on demand. The modals that act on the session close when it ends,
41
+ > and form fields are checked before sending.
42
+ >
35
43
  > Earlier: **0.11.3** required `@pollar/core@^0.11.3`. A **pre-built `PollarClient`
36
44
  > passed to `PollarProvider` no longer loses passkey support**: the provider installs the
37
45
  > browser passkey ceremony on every path (via core's new `setPasskeyDefaults()`), and an
package/dist/index.d.mts CHANGED
@@ -37,6 +37,14 @@ interface PollarContextValue {
37
37
  getClient: () => PollarClient;
38
38
  openLoginModal: () => void;
39
39
  isAuthenticated: boolean;
40
+ /**
41
+ * Whether the signed-in user has a wallet. False for an app that creates
42
+ * wallets on demand until {@link createWallet} runs, and always for one that
43
+ * creates none.
44
+ */
45
+ hasWallet: boolean;
46
+ /** Create the signed-in user's wallet; see `PollarClient.createWallet`. */
47
+ createWallet: () => Promise<WalletInfo>;
40
48
  /**
41
49
  * `true` once the server has confirmed the session (login / refresh /
42
50
  * `/auth/session/resume`). `false` while a cold-start session is still
@@ -305,6 +313,16 @@ interface WalletButtonTemplateProps {
305
313
  */
306
314
  notReadyReason?: string | null;
307
315
  creatingAccount: boolean;
316
+ /**
317
+ * Signed in with no wallet yet (an app that creates wallets on demand, or
318
+ * none). The button then shows the account menu instead of Login, with Create
319
+ * wallet only when `onCreateWallet` is set. Optional, so a custom template
320
+ * written before this existed keeps compiling.
321
+ */
322
+ signedIn?: boolean;
323
+ creatingWallet?: boolean;
324
+ createWalletError?: string | null;
325
+ onCreateWallet?: () => void;
308
326
  onToggleOpen: () => void;
309
327
  onCreateAccount: () => void;
310
328
  onCopy: () => void;
@@ -319,7 +337,7 @@ interface WalletButtonTemplateProps {
319
337
  onLogout: () => void;
320
338
  onLogin: () => void;
321
339
  }
322
- declare function WalletButtonTemplate({ walletAddress, accentColor, open, copied, dropdownBg, dropdownBorder, itemColor, wrapperRef, isInProgress, showCreateAccount, notReadyReason, creatingAccount, onToggleOpen, onCreateAccount, onCopy, onWalletBalance, onTxHistory, onSend, onReceive, onSessions, onKyc, onRamp, onDistributionRules, onLogout, onLogin, }: WalletButtonTemplateProps): react_jsx_runtime.JSX.Element;
340
+ declare function WalletButtonTemplate({ walletAddress, accentColor, open, copied, dropdownBg, dropdownBorder, itemColor, wrapperRef, isInProgress, showCreateAccount, notReadyReason, creatingAccount, onToggleOpen, onCreateAccount, onCopy, onWalletBalance, onTxHistory, onSend, onReceive, onSessions, onKyc, onRamp, onDistributionRules, onLogout, onLogin, signedIn, creatingWallet, createWalletError, onCreateWallet, }: WalletButtonTemplateProps): react_jsx_runtime.JSX.Element;
323
341
 
324
342
  /**
325
343
  * The chain a record lives on, with the legacy shape folded in: sessions and
package/dist/index.d.ts CHANGED
@@ -37,6 +37,14 @@ interface PollarContextValue {
37
37
  getClient: () => PollarClient;
38
38
  openLoginModal: () => void;
39
39
  isAuthenticated: boolean;
40
+ /**
41
+ * Whether the signed-in user has a wallet. False for an app that creates
42
+ * wallets on demand until {@link createWallet} runs, and always for one that
43
+ * creates none.
44
+ */
45
+ hasWallet: boolean;
46
+ /** Create the signed-in user's wallet; see `PollarClient.createWallet`. */
47
+ createWallet: () => Promise<WalletInfo>;
40
48
  /**
41
49
  * `true` once the server has confirmed the session (login / refresh /
42
50
  * `/auth/session/resume`). `false` while a cold-start session is still
@@ -305,6 +313,16 @@ interface WalletButtonTemplateProps {
305
313
  */
306
314
  notReadyReason?: string | null;
307
315
  creatingAccount: boolean;
316
+ /**
317
+ * Signed in with no wallet yet (an app that creates wallets on demand, or
318
+ * none). The button then shows the account menu instead of Login, with Create
319
+ * wallet only when `onCreateWallet` is set. Optional, so a custom template
320
+ * written before this existed keeps compiling.
321
+ */
322
+ signedIn?: boolean;
323
+ creatingWallet?: boolean;
324
+ createWalletError?: string | null;
325
+ onCreateWallet?: () => void;
308
326
  onToggleOpen: () => void;
309
327
  onCreateAccount: () => void;
310
328
  onCopy: () => void;
@@ -319,7 +337,7 @@ interface WalletButtonTemplateProps {
319
337
  onLogout: () => void;
320
338
  onLogin: () => void;
321
339
  }
322
- declare function WalletButtonTemplate({ walletAddress, accentColor, open, copied, dropdownBg, dropdownBorder, itemColor, wrapperRef, isInProgress, showCreateAccount, notReadyReason, creatingAccount, onToggleOpen, onCreateAccount, onCopy, onWalletBalance, onTxHistory, onSend, onReceive, onSessions, onKyc, onRamp, onDistributionRules, onLogout, onLogin, }: WalletButtonTemplateProps): react_jsx_runtime.JSX.Element;
340
+ declare function WalletButtonTemplate({ walletAddress, accentColor, open, copied, dropdownBg, dropdownBorder, itemColor, wrapperRef, isInProgress, showCreateAccount, notReadyReason, creatingAccount, onToggleOpen, onCreateAccount, onCopy, onWalletBalance, onTxHistory, onSend, onReceive, onSessions, onKyc, onRamp, onDistributionRules, onLogout, onLogin, signedIn, creatingWallet, createWalletError, onCreateWallet, }: WalletButtonTemplateProps): react_jsx_runtime.JSX.Element;
323
341
 
324
342
  /**
325
343
  * The chain a record lives on, with the legacy shape folded in: sessions and
package/dist/index.js CHANGED
@@ -1038,7 +1038,7 @@ var PollarModalFooter = ({ shield = false }) => {
1038
1038
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "pollar-footer-name", children: "Pollar" }),
1039
1039
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "pollar-footer-version", children: [
1040
1040
  "v",
1041
- "0.11.4-rc.4"
1041
+ "0.11.4"
1042
1042
  ] })
1043
1043
  ] })
1044
1044
  ] });
@@ -2264,15 +2264,35 @@ function initialValues(fields, answers) {
2264
2264
  return values;
2265
2265
  }
2266
2266
  var DATE = /^\d{4}-\d{2}-\d{2}$/;
2267
+ function localFieldError(field, value) {
2268
+ if (typeof value !== "string" || !value.trim()) return null;
2269
+ const text = value.trim();
2270
+ const rules = field.validation ?? {};
2271
+ if (field.type === "number") {
2272
+ const number = Number(text);
2273
+ if (!Number.isFinite(number)) return "invalid_type";
2274
+ if (rules.min !== void 0 && number < rules.min) return "too_small";
2275
+ if (rules.max !== void 0 && number > rules.max) return "too_large";
2276
+ return null;
2277
+ }
2278
+ if (field.type === "date") {
2279
+ return DATE.test(text) && !Number.isNaN(Date.parse(`${text}T00:00:00Z`)) ? null : "invalid_format";
2280
+ }
2281
+ if (field.type === "select" || field.type === "multiselect" || field.type === "checkbox") return null;
2282
+ if (rules.maxLength !== void 0 && text.length > rules.maxLength) return "too_long";
2283
+ if (rules.pattern) {
2284
+ try {
2285
+ if (!new RegExp(rules.pattern).test(text)) return "pattern";
2286
+ } catch {
2287
+ }
2288
+ }
2289
+ return null;
2290
+ }
2267
2291
  function localFieldErrors(fields, values) {
2268
2292
  const errors = {};
2269
2293
  for (const field of fields) {
2270
- const value = values[field.key];
2271
- if (typeof value !== "string" || !value.trim()) continue;
2272
- if (field.type === "number" && !Number.isFinite(Number(value))) errors[field.key] = "invalid_type";
2273
- if (field.type === "date" && (!DATE.test(value.trim()) || Number.isNaN(Date.parse(`${value.trim()}T00:00:00Z`)))) {
2274
- errors[field.key] = "invalid_format";
2275
- }
2294
+ const error = localFieldError(field, values[field.key]);
2295
+ if (error) errors[field.key] = error;
2276
2296
  }
2277
2297
  return errors;
2278
2298
  }
@@ -2352,7 +2372,8 @@ function FieldInput({
2352
2372
  value,
2353
2373
  language,
2354
2374
  disabled,
2355
- onChange
2375
+ onChange,
2376
+ onBlur
2356
2377
  }) {
2357
2378
  const id = `pollar-form-${field.key}`;
2358
2379
  const text = typeof value === "string" ? value : "";
@@ -2386,7 +2407,8 @@ function FieldInput({
2386
2407
  value: text,
2387
2408
  disabled,
2388
2409
  maxLength: field.validation?.maxLength,
2389
- onChange: (e) => onChange(e.target.value)
2410
+ onChange: (e) => onChange(e.target.value),
2411
+ onBlur
2390
2412
  }
2391
2413
  );
2392
2414
  default:
@@ -2402,7 +2424,8 @@ function FieldInput({
2402
2424
  min: field.validation?.min,
2403
2425
  max: field.validation?.max,
2404
2426
  maxLength: field.validation?.maxLength,
2405
- onChange: (e) => onChange(e.target.value)
2427
+ onChange: (e) => onChange(e.target.value),
2428
+ onBlur
2406
2429
  }
2407
2430
  );
2408
2431
  }
@@ -2433,6 +2456,24 @@ function RequirementFormModal({ formId, progress, onClose, onSubmitted }) {
2433
2456
  setValues(initialValues(loaded.fields, loaded.answers));
2434
2457
  }).catch(() => mounted.current && setError(copy.loadError));
2435
2458
  }, [formId]);
2459
+ function recheck(field, value, onlyIfShown) {
2460
+ if (typeof value === "string" && !value.trim()) return;
2461
+ setFieldErrors((errors) => {
2462
+ if (onlyIfShown && !(field.key in errors)) return errors;
2463
+ const next = { ...errors };
2464
+ const error2 = localFieldError(field, value);
2465
+ if (error2) next[field.key] = error2;
2466
+ else delete next[field.key];
2467
+ return next;
2468
+ });
2469
+ }
2470
+ function change(field, value) {
2471
+ setValues((v) => ({ ...v, [field.key]: value }));
2472
+ recheck(field, value, true);
2473
+ }
2474
+ function blur(field) {
2475
+ recheck(field, values[field.key], false);
2476
+ }
2436
2477
  async function submit() {
2437
2478
  if (!form) return;
2438
2479
  setError(null);
@@ -2512,7 +2553,8 @@ function RequirementFormModal({ formId, progress, onClose, onSubmitted }) {
2512
2553
  value: values[field.key] ?? "",
2513
2554
  language,
2514
2555
  disabled: submitting,
2515
- onChange: (value) => setValues((v) => ({ ...v, [field.key]: value }))
2556
+ onChange: (value) => change(field, value),
2557
+ onBlur: () => blur(field)
2516
2558
  }
2517
2559
  )
2518
2560
  ] }),
@@ -3971,6 +4013,7 @@ var REGISTRATION_COPY = {
3971
4013
  incomplete: "Your registration with the provider did not finish. Register again to complete it.",
3972
4014
  providerDown: "The provider is not responding right now. Please try again in a few minutes.",
3973
4015
  ipNotSupported: "The provider cannot accept your current connection (IPv6). Please try again from another network, for example Wi-Fi.",
4016
+ emailTaken: "This email is already registered with the provider through another app. Use a different email to register here.",
3974
4017
  loadError: "Could not load the registration. Please try again.",
3975
4018
  submitError: "Could not register you. Please try again.",
3976
4019
  step: "Step {n} of {total}",
@@ -3990,6 +4033,7 @@ var REGISTRATION_COPY = {
3990
4033
  incomplete: "Tu registro con el proveedor no se complet\xF3. Reg\xEDstrate de nuevo para terminarlo.",
3991
4034
  providerDown: "El proveedor no responde en este momento. Intenta de nuevo en unos minutos.",
3992
4035
  ipNotSupported: "El proveedor no acepta tu conexi\xF3n actual (IPv6). Intenta desde otra red, por ejemplo Wi-Fi.",
4036
+ emailTaken: "Este email ya est\xE1 registrado con el proveedor desde otra app. Usa otro email para registrarte aqu\xED.",
3993
4037
  loadError: "No se pudo cargar el registro. Intenta de nuevo.",
3994
4038
  submitError: "No se pudo completar el registro. Intenta de nuevo.",
3995
4039
  step: "Paso {n} de {total}",
@@ -4312,6 +4356,7 @@ function ProviderRegistrationModal({
4312
4356
  } else if (code === "KYC_REGISTRATION_STEPS_PENDING") setError(copy.notReady);
4313
4357
  else if (code === "SDK_CARDS_PROVIDER_ERROR") setError(copy.providerDown);
4314
4358
  else if (code === "SDK_CARDS_IP_NOT_SUPPORTED") setError(copy.ipNotSupported);
4359
+ else if (code === "SDK_CARDS_PROVIDER_USER_TAKEN") setError(copy.emailTaken);
4315
4360
  else setError(copy.submitError);
4316
4361
  } finally {
4317
4362
  if (mounted.current) setSubmitting(false);
@@ -7017,7 +7062,8 @@ function groupPan(pan) {
7017
7062
  }
7018
7063
  var CARD_ERROR_COPY = {
7019
7064
  SDK_CARDS_PROVIDER_ERROR: "The card provider is not responding right now. Please try again in a few minutes.",
7020
- SDK_CARDS_IP_NOT_SUPPORTED: "The card provider cannot accept your current connection (IPv6). Please try again from another network, for example Wi-Fi."
7065
+ SDK_CARDS_IP_NOT_SUPPORTED: "The card provider cannot accept your current connection (IPv6). Please try again from another network, for example Wi-Fi.",
7066
+ SDK_CARDS_PROVIDER_USER_TAKEN: "This email is already registered with the card provider through another app. Use a different email to get a card here."
7021
7067
  };
7022
7068
  function errorText(e, fallback) {
7023
7069
  if (core.isPollarApiError(e)) {
@@ -8633,7 +8679,7 @@ function PollarProvider({
8633
8679
  triggered = false;
8634
8680
  return;
8635
8681
  }
8636
- if (triggered || sessionRef.current?.wallet?.address) return;
8682
+ if (triggered || sessionRef.current) return;
8637
8683
  triggered = true;
8638
8684
  pollarClient.login({ provider: id });
8639
8685
  })
@@ -8712,6 +8758,7 @@ function PollarProvider({
8712
8758
  );
8713
8759
  const walletAddress = sessionState?.wallet?.address || "";
8714
8760
  const walletProvisioning = sessionState?.wallet?.provisioning;
8761
+ const hasSession = sessionState !== null;
8715
8762
  const getClient = react.useCallback(() => pollarClient, [pollarClient]);
8716
8763
  const refreshWalletBalance = react.useCallback(() => pollarClient.refreshBalance(), [pollarClient, walletAddress]);
8717
8764
  const refreshAssets = react.useCallback(() => pollarClient.refreshAssets(), [pollarClient, walletAddress]);
@@ -8721,7 +8768,9 @@ function PollarProvider({
8721
8768
  // session
8722
8769
  wallet: pollarClient.getWallet(),
8723
8770
  wallets: pollarClient.getWallets(),
8724
- isAuthenticated: !!walletAddress,
8771
+ isAuthenticated: hasSession,
8772
+ hasWallet: pollarClient.hasWallet(),
8773
+ createWallet: () => pollarClient.createWallet(),
8725
8774
  verified,
8726
8775
  // client
8727
8776
  getClient,
@@ -8809,6 +8858,7 @@ function PollarProvider({
8809
8858
  }, [
8810
8859
  walletAddress,
8811
8860
  walletProvisioning,
8861
+ hasSession,
8812
8862
  verified,
8813
8863
  pollarClient,
8814
8864
  getClient,
@@ -8924,8 +8974,54 @@ function WalletButtonTemplate({
8924
8974
  onRamp,
8925
8975
  onDistributionRules,
8926
8976
  onLogout,
8927
- onLogin
8977
+ onLogin,
8978
+ signedIn,
8979
+ creatingWallet,
8980
+ createWalletError,
8981
+ onCreateWallet
8928
8982
  }) {
8983
+ if (!walletAddress && signedIn) {
8984
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pollar-wallet-wrapper", ref: wrapperRef, children: [
8985
+ /* @__PURE__ */ jsxRuntime.jsxs("button", { className: "pollar-wallet-btn", style: { backgroundColor: accentColor }, onClick: onToggleOpen, children: [
8986
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "pollar-wallet-btn-label", children: onCreateWallet ? "No wallet" : "Signed in" }),
8987
+ /* @__PURE__ */ jsxRuntime.jsx(
8988
+ "svg",
8989
+ {
8990
+ className: `pollar-wallet-chevron${open ? " pollar-open" : ""}`,
8991
+ viewBox: "0 0 12 12",
8992
+ fill: "none",
8993
+ stroke: "currentColor",
8994
+ strokeWidth: "2",
8995
+ strokeLinecap: "round",
8996
+ strokeLinejoin: "round",
8997
+ children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "2,4 6,8 10,4" })
8998
+ }
8999
+ )
9000
+ ] }),
9001
+ open && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pollar-wallet-dropdown", style: { backgroundColor: dropdownBg, borderColor: dropdownBorder }, children: [
9002
+ createWalletError && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
9003
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pollar-wallet-dropdown-notice", style: { color: itemColor }, role: "status", children: createWalletError }),
9004
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pollar-wallet-dropdown-divider" })
9005
+ ] }),
9006
+ onCreateWallet && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
9007
+ /* @__PURE__ */ jsxRuntime.jsx(
9008
+ "button",
9009
+ {
9010
+ className: "pollar-wallet-dropdown-item",
9011
+ style: { color: itemColor },
9012
+ onClick: onCreateWallet,
9013
+ disabled: creatingWallet,
9014
+ children: creatingWallet ? "Creating wallet\u2026" : "Create wallet"
9015
+ }
9016
+ ),
9017
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pollar-wallet-dropdown-divider" })
9018
+ ] }),
9019
+ /* @__PURE__ */ jsxRuntime.jsx("button", { className: "pollar-wallet-dropdown-item", style: { color: itemColor }, onClick: onSessions, children: "Active sessions" }),
9020
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pollar-wallet-dropdown-divider" }),
9021
+ /* @__PURE__ */ jsxRuntime.jsx("button", { className: "pollar-wallet-dropdown-item pollar-danger", onClick: onLogout, children: "Log out" })
9022
+ ] })
9023
+ ] });
9024
+ }
8929
9025
  if (!walletAddress) {
8930
9026
  return /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "pollar-wallet-login-btn", style: { backgroundColor: accentColor }, onClick: onLogin, children: [
8931
9027
  /* @__PURE__ */ jsxRuntime.jsx(ButtonLogo, {}),
@@ -9246,7 +9342,10 @@ function WalletButton() {
9246
9342
  openKycModal,
9247
9343
  openRampModal,
9248
9344
  openDistributionRulesModal,
9249
- tx: transaction
9345
+ tx: transaction,
9346
+ isAuthenticated,
9347
+ createWallet,
9348
+ appConfig
9250
9349
  } = usePollar();
9251
9350
  const { primaryChain, primaryAddress } = useChains();
9252
9351
  const walletAddress = primaryAddress || (wallet?.address ?? "");
@@ -9254,6 +9353,8 @@ function WalletButton() {
9254
9353
  const [copied, setCopied] = react.useState(false);
9255
9354
  const [creating, setCreating] = react.useState(false);
9256
9355
  const [created, setCreated] = react.useState(false);
9356
+ const [creatingWallet, setCreatingWallet] = react.useState(false);
9357
+ const [createWalletError, setCreateWalletError] = react.useState(null);
9257
9358
  const wrapperRef = react.useRef(null);
9258
9359
  const copyTimerRef = react.useRef(null);
9259
9360
  const isInProgress = transaction.step === "building" || transaction.step === "signing";
@@ -9307,6 +9408,21 @@ function WalletButton() {
9307
9408
  setCreating(false);
9308
9409
  }
9309
9410
  }
9411
+ async function handleCreateWallet() {
9412
+ if (creatingWallet) return;
9413
+ setCreatingWallet(true);
9414
+ setCreateWalletError(null);
9415
+ try {
9416
+ await createWallet();
9417
+ setOpen(false);
9418
+ } catch (err) {
9419
+ setCreateWalletError(
9420
+ core.isPollarApiError(err) && err.code === "SDK_WALLET_PROVISIONING_DISABLED" ? "This app does not create wallets." : "Could not create the wallet. Try again."
9421
+ );
9422
+ } finally {
9423
+ setCreatingWallet(false);
9424
+ }
9425
+ }
9310
9426
  function handleWalletBalance() {
9311
9427
  setOpen(false);
9312
9428
  openWalletBalanceModal();
@@ -9352,6 +9468,10 @@ function WalletButton() {
9352
9468
  wrapperRef,
9353
9469
  isInProgress,
9354
9470
  showCreateAccount: canCreateAccount,
9471
+ signedIn: isAuthenticated,
9472
+ creatingWallet,
9473
+ createWalletError,
9474
+ ...appConfig.application?.walletProvisioning === "NONE" ? {} : { onCreateWallet: handleCreateWallet },
9355
9475
  notReadyReason,
9356
9476
  creatingAccount: creating,
9357
9477
  onToggleOpen: () => setOpen((v) => !v),