@proveanything/smartlinks-auth-ui 0.4.7 → 0.4.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.
package/dist/index.esm.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import React, { useEffect, useState, useMemo, useRef, useCallback, createContext, useContext } from 'react';
3
3
  import * as smartlinks from '@proveanything/smartlinks';
4
- import { iframe, SmartlinksApiError } from '@proveanything/smartlinks';
4
+ import { SmartlinksApiError, iframe } from '@proveanything/smartlinks';
5
5
  import { post, getApiHeaders, isProxyEnabled } from '@proveanything/smartlinks/dist/http';
6
6
 
7
7
  const AuthContainer = ({ children, theme = 'light', className = '', config, minimal = false, }) => {
@@ -10759,7 +10759,8 @@ const PhoneInput = ({ value, onChange, disabled = false, }) => {
10759
10759
  return (jsx(PhoneInputComponent, { international: true, defaultCountry: defaultCountry, value: value, onChange: (value) => onChange(value || ''), disabled: disabled, className: "phone-input-wrapper" }));
10760
10760
  };
10761
10761
 
10762
- const PhoneAuthForm = ({ onSubmit, onBack, loading, error, }) => {
10762
+ const PhoneAuthForm = ({ onSubmit, onBack, loading, error, collectName = false, }) => {
10763
+ const [displayName, setDisplayName] = useState('');
10763
10764
  const [phoneNumber, setPhoneNumber] = useState('');
10764
10765
  const [verificationCode, setVerificationCode] = useState('');
10765
10766
  const [codeSent, setCodeSent] = useState(false);
@@ -10776,7 +10777,8 @@ const PhoneAuthForm = ({ onSubmit, onBack, loading, error, }) => {
10776
10777
  const handleSendCode = async (e) => {
10777
10778
  e.preventDefault();
10778
10779
  try {
10779
- await onSubmit(phoneNumber);
10780
+ const normalizedDisplayName = displayName.trim();
10781
+ await onSubmit(phoneNumber, undefined, collectName ? normalizedDisplayName || undefined : undefined);
10780
10782
  // Only transition to code entry UI AFTER successful send
10781
10783
  setCodeSent(true);
10782
10784
  setResendCooldown(60); // 60 second cooldown
@@ -10803,7 +10805,7 @@ const PhoneAuthForm = ({ onSubmit, onBack, loading, error, }) => {
10803
10805
  };
10804
10806
  return (jsxs("form", { className: "auth-form", onSubmit: codeSent ? handleVerifyCode : handleSendCode, children: [jsxs("div", { className: "auth-form-header", children: [jsx("h2", { className: "auth-form-title", children: "Phone Authentication" }), jsx("p", { className: "auth-form-subtitle", children: codeSent
10805
10807
  ? 'Enter the verification code sent to your phone.'
10806
- : 'Enter your phone number to receive a verification code.' })] }), error && (jsxs("div", { className: "auth-error", role: "alert", children: [jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: jsx("path", { d: "M8 0C3.58 0 0 3.58 0 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z" }) }), error] })), !codeSent ? (jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "phoneNumber", className: "auth-label", children: "Phone Number" }), jsx(PhoneInput, { value: phoneNumber, onChange: setPhoneNumber, disabled: loading })] })) : (jsxs(Fragment, { children: [jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "verificationCode", className: "auth-label", children: "Verification Code" }), jsx(OTPInput, { length: 6, value: verificationCode, onChange: setVerificationCode, disabled: loading })] }), jsx("div", { style: { marginTop: '0.5rem', textAlign: 'center' }, children: jsx("button", { type: "button", className: "auth-link", onClick: handleResendCode, disabled: loading || resendCooldown > 0, style: {
10808
+ : 'Enter your phone number to receive a verification code.' })] }), error && (jsxs("div", { className: "auth-error", role: "alert", children: [jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: jsx("path", { d: "M8 0C3.58 0 0 3.58 0 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z" }) }), error] })), !codeSent ? (jsxs(Fragment, { children: [collectName && (jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "phoneName", className: "auth-label", children: "Name" }), jsx("input", { id: "phoneName", type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), className: "auth-input", placeholder: "John Smith", disabled: loading, autoComplete: "name" })] })), jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "phoneNumber", className: "auth-label", children: "Phone Number" }), jsx(PhoneInput, { value: phoneNumber, onChange: setPhoneNumber, disabled: loading })] })] })) : (jsxs(Fragment, { children: [jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "verificationCode", className: "auth-label", children: "Verification Code" }), jsx(OTPInput, { length: 6, value: verificationCode, onChange: setVerificationCode, disabled: loading })] }), jsx("div", { style: { marginTop: '0.5rem', textAlign: 'center' }, children: jsx("button", { type: "button", className: "auth-link", onClick: handleResendCode, disabled: loading || resendCooldown > 0, style: {
10807
10809
  cursor: resendCooldown > 0 ? 'not-allowed' : 'pointer',
10808
10810
  opacity: resendCooldown > 0 ? 0.5 : 1,
10809
10811
  }, children: resendCooldown > 0
@@ -10846,13 +10848,15 @@ const PasswordResetForm = ({ onSubmit, onBack, loading, error, success, successM
10846
10848
  : "Enter your email address and we'll send you instructions to reset your password." })] }), (error || passwordError) && (jsxs("div", { className: "auth-error", role: "alert", children: [jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: jsx("path", { d: "M8 0C3.58 0 0 3.58 0 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z" }) }), error || passwordError] })), token ? (jsxs(Fragment, { children: [resetEmail && (jsx("input", { type: "email", value: resetEmail, readOnly: true, autoComplete: "username", style: { position: 'absolute', opacity: 0, height: 0, width: 0, overflow: 'hidden', pointerEvents: 'none' }, tabIndex: -1, "aria-hidden": "true" })), jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "password", className: "auth-label", children: "New Password" }), jsx("input", { type: "password", id: "password", className: "auth-input", value: password, onChange: (e) => setPassword(e.target.value), required: true, disabled: loading, placeholder: "Enter new password", autoComplete: "new-password", minLength: 6 })] }), jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "confirmPassword", className: "auth-label", children: "Confirm Password" }), jsx("input", { type: "password", id: "confirmPassword", className: "auth-input", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), required: true, disabled: loading, placeholder: "Confirm new password", autoComplete: "new-password", minLength: 6 })] })] })) : (jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "email", className: "auth-label", children: "Email address" }), jsx("input", { type: "email", id: "email", className: "auth-input", value: email, onChange: (e) => setEmail(e.target.value), required: true, disabled: loading, placeholder: "you@example.com", autoComplete: "email" })] })), jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading, children: loading ? (jsx("span", { className: "auth-spinner" })) : token ? ('Reset password') : ('Send reset instructions') }), jsx("div", { className: "auth-divider", children: jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onBack, disabled: loading, children: "\u2190 Back to Sign in" }) })] }));
10847
10849
  };
10848
10850
 
10849
- const MagicLinkForm = ({ onSubmit, onCancel, loading = false, error, }) => {
10851
+ const MagicLinkForm = ({ onSubmit, onCancel, loading = false, error, collectName = false, }) => {
10850
10852
  const [email, setEmail] = useState('');
10853
+ const [displayName, setDisplayName] = useState('');
10851
10854
  const handleSubmit = async (e) => {
10852
10855
  e.preventDefault();
10853
- await onSubmit(email);
10856
+ const normalizedDisplayName = displayName.trim();
10857
+ await onSubmit(email, collectName ? normalizedDisplayName || undefined : undefined);
10854
10858
  };
10855
- return (jsxs("form", { onSubmit: handleSubmit, className: "auth-form", children: [jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "magic-link-email", className: "auth-label", children: "Email Address" }), jsx("input", { id: "magic-link-email", type: "email", value: email, onChange: (e) => setEmail(e.target.value), className: "auth-input", placeholder: "you@example.com", required: true, disabled: loading })] }), error && (jsx("div", { className: "auth-error-message", children: error })), jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading || !email, children: loading ? (jsxs(Fragment, { children: [jsx("span", { className: "auth-spinner" }), "Sending..."] })) : ('Send Magic Link') }), jsx("button", { type: "button", onClick: onCancel, className: "auth-button auth-button-secondary", disabled: loading, children: "Cancel" })] }));
10859
+ return (jsxs("form", { onSubmit: handleSubmit, className: "auth-form", children: [collectName && (jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "magic-link-name", className: "auth-label", children: "Name" }), jsx("input", { id: "magic-link-name", type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), className: "auth-input", placeholder: "John Smith", disabled: loading, autoComplete: "name" })] })), jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "magic-link-email", className: "auth-label", children: "Email Address" }), jsx("input", { id: "magic-link-email", type: "email", value: email, onChange: (e) => setEmail(e.target.value), className: "auth-input", placeholder: "you@example.com", required: true, disabled: loading })] }), error && (jsx("div", { className: "auth-error-message", children: error })), jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading || !email, children: loading ? (jsxs(Fragment, { children: [jsx("span", { className: "auth-spinner" }), "Sending..."] })) : ('Send Magic Link') }), jsx("button", { type: "button", onClick: onCancel, className: "auth-button auth-button-secondary", disabled: loading, children: "Cancel" })] }));
10856
10860
  };
10857
10861
 
10858
10862
  /**
@@ -10889,6 +10893,254 @@ function createLoggerWrapper(logger) {
10889
10893
  };
10890
10894
  }
10891
10895
 
10896
+ /**
10897
+ * Friendly error messages for common HTTP status codes.
10898
+ */
10899
+ const STATUS_MESSAGES = {
10900
+ 400: 'Invalid request. Please check your input and try again.',
10901
+ 401: 'Invalid credentials. Please check your email and password.',
10902
+ 403: 'Access denied. You do not have permission to perform this action.',
10903
+ 404: 'Account not found. Please check your email or create a new account.',
10904
+ 409: 'This email is already registered.',
10905
+ 429: 'Too many attempts. Please wait a moment and try again.',
10906
+ };
10907
+ /**
10908
+ * Context-specific error messages for different auth operations.
10909
+ */
10910
+ const ERROR_CODE_MESSAGES = {
10911
+ // 400 - Validation errors
10912
+ 'MISSING_FIELDS': 'Email and password are required.',
10913
+ 'MISSING_EMAIL': 'Email is required.',
10914
+ 'MISSING_PASSWORD': 'Password is required.',
10915
+ 'MISSING_TOKEN': 'Token is required.',
10916
+ 'MISSING_PHONE_NUMBER': 'Phone number is required.',
10917
+ 'MISSING_VERIFICATION_CODE': 'Phone number and verification code are required.',
10918
+ 'MISSING_REDIRECT_URL': 'Redirect URL is required.',
10919
+ 'MISSING_GOOGLE_TOKEN': 'Google token is required.',
10920
+ 'INVALID_CLIENT_ID': 'Invalid client configuration.',
10921
+ 'INVALID_REDIRECT_URL': 'Invalid redirect URL.',
10922
+ 'INVALID_PHONE_NUMBER': 'Invalid phone number. Please check the format and try again.',
10923
+ 'INVALID_GOOGLE_TOKEN': 'Invalid Google sign-in token.',
10924
+ 'PASSWORD_TOO_SHORT': 'Password must be at least 8 characters long.',
10925
+ 'PASSWORD_REQUIREMENTS_NOT_MET': 'New password must be at least 6 characters.',
10926
+ 'EMAIL_ALREADY_VERIFIED': 'Your email is already verified.',
10927
+ 'INVALID_CONFIRMATION': 'Please type DELETE to confirm account deletion.',
10928
+ // 401 - Authentication errors
10929
+ 'INVALID_CREDENTIALS': 'Invalid email or password.',
10930
+ 'INCORRECT_PASSWORD': 'Current password is incorrect.',
10931
+ 'INVALID_VERIFICATION_CODE': 'Invalid or expired verification code. Please try again.',
10932
+ 'INVALID_TOKEN': 'This link has expired or is invalid. Please request a new one.',
10933
+ 'TOKEN_EXPIRED': 'This link has expired. Please request a new one.',
10934
+ 'TOKEN_ALREADY_USED': 'This link has already been used. Please request a new one.',
10935
+ 'UNAUTHORIZED': 'You must be logged in to perform this action.',
10936
+ 'GOOGLE_TOKEN_AUDIENCE_MISMATCH': 'Google sign-in failed. Token was not issued for this application.',
10937
+ // 403 - Forbidden / verification required
10938
+ 'EMAIL_NOT_VERIFIED': 'Please verify your email before signing in.',
10939
+ 'ACCOUNT_LOCKED': 'Your account has been locked. Please verify your email to unlock.',
10940
+ 'EMAIL_VERIFICATION_EXPIRED': 'Your verification deadline has passed and your account is locked. Please contact support.',
10941
+ // 404
10942
+ 'USER_NOT_FOUND': 'Account not found. Please check your email or create a new account.',
10943
+ // 409 - Conflicts
10944
+ 'EMAIL_ALREADY_EXISTS': 'This email is already registered.',
10945
+ 'EMAIL_IN_USE': 'This email is already in use.',
10946
+ // 429 - Rate limiting
10947
+ 'RATE_LIMIT_EXCEEDED': 'Too many requests. Please try again later.',
10948
+ 'TOO_MANY_MAGIC_LINKS': 'Too many magic link requests. Please try again later.',
10949
+ 'TOO_MANY_VERIFICATION_ATTEMPTS': 'Too many verification attempts. Please wait and try again.',
10950
+ 'MAX_VERIFICATION_ATTEMPTS': 'Maximum verification attempts reached. Please try again later.',
10951
+ // 500 - Server errors
10952
+ 'LOGIN_FAILED': 'Login failed. Please try again later.',
10953
+ 'REGISTRATION_FAILED': 'Registration failed. Please try again later.',
10954
+ 'GOOGLE_AUTH_NOT_CONFIGURED': 'Google sign-in is not available for this application.',
10955
+ 'GOOGLE_AUTH_FAILED': 'Google sign-in failed. Please try again.',
10956
+ 'GOOGLE_USERINFO_FAILED': 'Failed to retrieve your Google account information. Please try again.',
10957
+ 'PHONE_VERIFICATION_FAILED': 'Phone verification failed. Please try again.',
10958
+ 'SEND_VERIFICATION_CODE_FAILED': 'Failed to send verification code. Please try again.',
10959
+ 'MAGIC_LINK_SEND_FAILED': 'Failed to send magic link. Please try again.',
10960
+ 'MAGIC_LINK_VERIFICATION_FAILED': 'Magic link verification failed. Please try again.',
10961
+ 'PASSWORD_RESET_FAILED': 'Failed to process password reset. Please try again.',
10962
+ 'PASSWORD_RESET_COMPLETE_FAILED': 'Failed to reset password. Please try again.',
10963
+ 'EMAIL_VERIFICATION_SEND_FAILED': 'Failed to send verification email. Please try again.',
10964
+ 'EMAIL_VERIFICATION_FAILED': 'Email verification failed. Please try again.',
10965
+ // Account management 500s
10966
+ 'UPDATE_PROFILE_FAILED': 'Failed to update profile. Please try again.',
10967
+ 'CHANGE_PASSWORD_FAILED': 'Failed to change password. Please try again.',
10968
+ 'CHANGE_EMAIL_FAILED': 'Failed to change email. Please try again.',
10969
+ 'UPDATE_PHONE_FAILED': 'Failed to update phone number. Please try again.',
10970
+ 'DELETE_ACCOUNT_FAILED': 'Failed to delete account. Please try again.',
10971
+ 'CONFIG_FETCH_FAILED': 'Failed to load configuration. Please try again.',
10972
+ 'INTERNAL_ERROR': 'An unexpected error occurred. Please try again.',
10973
+ // Legacy aliases (kept for backward compatibility)
10974
+ 'INVALID_CODE': 'Invalid verification code. Please check and try again.',
10975
+ 'CODE_EXPIRED': 'This code has expired. Please request a new one.',
10976
+ 'PHONE_NOT_SUPPORTED': 'This phone number is not supported. Please try a different number.',
10977
+ 'INVALID_PHONE': 'Invalid phone number. Please check the format and try again.',
10978
+ 'PASSWORD_TOO_WEAK': 'Password is too weak. Please use at least 8 characters with a mix of letters and numbers.',
10979
+ 'RATE_LIMITED': 'Too many attempts. Please wait a moment and try again.',
10980
+ };
10981
+ function isApiErrorLike(error) {
10982
+ if (error instanceof SmartlinksApiError)
10983
+ return true;
10984
+ if (error && typeof error === 'object' && 'statusCode' in error && 'message' in error) {
10985
+ const e = error;
10986
+ return typeof e.statusCode === 'number' && typeof e.message === 'string';
10987
+ }
10988
+ return false;
10989
+ }
10990
+ /**
10991
+ * Extracts a user-friendly error message from an error.
10992
+ *
10993
+ * Handles:
10994
+ * - SmartlinksApiError (and duck-typed equivalents from proxy mode)
10995
+ * - Standard Error: Uses message property
10996
+ * - String: Passes through directly (for native bridge errors)
10997
+ * - Unknown: Returns generic message
10998
+ */
10999
+ function getFriendlyErrorMessage(error) {
11000
+ // Handle SmartlinksApiError or duck-typed API errors (proxy mode)
11001
+ if (isApiErrorLike(error)) {
11002
+ // First, check for specific error code (most precise)
11003
+ const errorCode = error.errorCode || error.details?.errorCode || error.details?.error;
11004
+ // For rate-limit errors, prefer the backend's message as it includes specific wait times
11005
+ if (errorCode === 'RATE_LIMIT_EXCEEDED' && error.message && error.message !== '[object Object]') {
11006
+ return error.message;
11007
+ }
11008
+ if (errorCode && ERROR_CODE_MESSAGES[errorCode]) {
11009
+ return ERROR_CODE_MESSAGES[errorCode];
11010
+ }
11011
+ // Then, check status code for general category messages
11012
+ if (error.statusCode >= 500) {
11013
+ return 'Server error. Please try again later.';
11014
+ }
11015
+ // For 429 status, prefer backend message (may include wait time)
11016
+ if (error.statusCode === 429 && error.message && error.message !== '[object Object]') {
11017
+ return error.message;
11018
+ }
11019
+ if (STATUS_MESSAGES[error.statusCode]) {
11020
+ return STATUS_MESSAGES[error.statusCode];
11021
+ }
11022
+ // Fall back to the server's message (already human-readable from backend)
11023
+ return error.message;
11024
+ }
11025
+ // Handle standard Error objects
11026
+ if (error instanceof Error) {
11027
+ // SDK bug workaround: SDK may do `throw new Error(responseBodyObject)` which produces
11028
+ // message "[object Object]". Check for API error properties attached to the Error instance.
11029
+ const errAny = error;
11030
+ // Check if the Error has API error properties directly attached (e.g., error.statusCode, error.errorCode)
11031
+ if (typeof errAny.statusCode === 'number' || errAny.errorCode || errAny.response) {
11032
+ // Try to extract from attached properties
11033
+ const apiLike = errAny.response || errAny;
11034
+ if (isApiErrorLike(apiLike)) {
11035
+ return getFriendlyErrorMessage(apiLike);
11036
+ }
11037
+ }
11038
+ // Check if the Error has a `cause` with API error details (modern Error cause pattern)
11039
+ if (errAny.cause && typeof errAny.cause === 'object') {
11040
+ if (isApiErrorLike(errAny.cause)) {
11041
+ return getFriendlyErrorMessage(errAny.cause);
11042
+ }
11043
+ }
11044
+ // If the message is "[object Object]", the error was constructed from a plain object
11045
+ // This is useless - return a generic message instead
11046
+ if (error.message === '[object Object]') {
11047
+ // Log the actual error for debugging
11048
+ console.warn('[AuthKit] Error with [object Object] message. Raw error:', JSON.stringify(errAny, Object.getOwnPropertyNames(errAny)));
11049
+ return 'An unexpected error occurred. Please try again.';
11050
+ }
11051
+ // Check if the message itself contains a known API error pattern
11052
+ if (/already (registered|exists)/i.test(error.message)) {
11053
+ return 'This email is already registered.';
11054
+ }
11055
+ return error.message;
11056
+ }
11057
+ // Handle plain strings (e.g., from native bridge callbacks)
11058
+ if (typeof error === 'string') {
11059
+ return error;
11060
+ }
11061
+ // Unknown error type
11062
+ return 'An unexpected error occurred. Please try again.';
11063
+ }
11064
+ /**
11065
+ * Checks if an error represents a conflict (409) - typically duplicate registration.
11066
+ */
11067
+ function isConflictError(error) {
11068
+ if (isApiErrorLike(error))
11069
+ return error.statusCode === 409;
11070
+ // Also check error message for keyword match (resilient fallback)
11071
+ if (error instanceof Error && /already (registered|exists)/i.test(error.message))
11072
+ return true;
11073
+ return false;
11074
+ }
11075
+ /**
11076
+ * Checks if an error represents invalid credentials (401).
11077
+ */
11078
+ function isAuthError(error) {
11079
+ if (error instanceof SmartlinksApiError)
11080
+ return error.isAuthError();
11081
+ if (isApiErrorLike(error))
11082
+ return error.statusCode === 401 || error.statusCode === 403;
11083
+ return false;
11084
+ }
11085
+ /**
11086
+ * Checks if an error represents rate limiting (429).
11087
+ */
11088
+ function isRateLimitError(error) {
11089
+ if (error instanceof SmartlinksApiError)
11090
+ return error.isRateLimited();
11091
+ if (isApiErrorLike(error))
11092
+ return error.statusCode === 429;
11093
+ return false;
11094
+ }
11095
+ /**
11096
+ * Checks if an error represents a server error (5xx).
11097
+ */
11098
+ function isServerError(error) {
11099
+ if (error instanceof SmartlinksApiError)
11100
+ return error.isServerError();
11101
+ if (isApiErrorLike(error))
11102
+ return error.statusCode >= 500;
11103
+ return false;
11104
+ }
11105
+ /**
11106
+ * Gets the HTTP status code from an error, if available.
11107
+ */
11108
+ function getErrorStatusCode(error) {
11109
+ if (isApiErrorLike(error))
11110
+ return error.statusCode;
11111
+ return undefined;
11112
+ }
11113
+ /**
11114
+ * Gets the server-specific error code from an error, if available.
11115
+ */
11116
+ function getErrorCode(error) {
11117
+ if (isApiErrorLike(error)) {
11118
+ return error.errorCode || error.details?.errorCode || error.details?.error;
11119
+ }
11120
+ return undefined;
11121
+ }
11122
+ /**
11123
+ * Error codes that indicate the user needs to verify their email.
11124
+ */
11125
+ const EMAIL_VERIFICATION_ERROR_CODES = new Set([
11126
+ 'EMAIL_NOT_VERIFIED',
11127
+ 'ACCOUNT_LOCKED',
11128
+ 'EMAIL_VERIFICATION_EXPIRED',
11129
+ ]);
11130
+ /**
11131
+ * Checks if an error requires email verification action from the user.
11132
+ */
11133
+ function requiresEmailVerification(error) {
11134
+ const code = getErrorCode(error);
11135
+ if (code && EMAIL_VERIFICATION_ERROR_CODES.has(code))
11136
+ return true;
11137
+ // Also check the flag from the response body
11138
+ if (error && typeof error === 'object' && 'requiresEmailVerification' in error) {
11139
+ return error.requiresEmailVerification === true;
11140
+ }
11141
+ return false;
11142
+ }
11143
+
10892
11144
  /**
10893
11145
  * AuthAPI - Thin wrapper around Smartlinks SDK authKit namespace
10894
11146
  * All authentication operations now use the global Smartlinks SDK
@@ -11034,6 +11286,36 @@ class AuthAPI {
11034
11286
  };
11035
11287
  }
11036
11288
  }
11289
+ /**
11290
+ * Ensure an account exists for the given email (or phone).
11291
+ * Calls register and silently handles 409 (account already exists).
11292
+ * This enables passwordless flows (magic link, phone) to work for new users.
11293
+ */
11294
+ async ensureAccount(data) {
11295
+ try {
11296
+ // Generate a random password since passwordless users won't use it
11297
+ const randomPassword = crypto.randomUUID?.() || Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
11298
+ // Backend requires displayName to be a valid string
11299
+ // Fall back to email local part if no name provided
11300
+ const normalizedDisplayName = typeof data.displayName === 'string' ? data.displayName.trim() : '';
11301
+ const fallbackName = normalizedDisplayName || (data.email ? data.email.split('@')[0] : 'User');
11302
+ await this.register({
11303
+ email: data.email,
11304
+ password: randomPassword,
11305
+ displayName: fallbackName,
11306
+ });
11307
+ this.log.log('ensureAccount: new account created for', data.email || data.phoneNumber);
11308
+ }
11309
+ catch (err) {
11310
+ // 409 = account already exists, which is fine
11311
+ if (isConflictError(err)) {
11312
+ this.log.log('ensureAccount: account already exists for', data.email || data.phoneNumber);
11313
+ return;
11314
+ }
11315
+ // Re-throw any other error
11316
+ throw err;
11317
+ }
11318
+ }
11037
11319
  async sendMagicLink(email, redirectUrl) {
11038
11320
  return smartlinks.authKit.sendMagicLink(this.clientId, {
11039
11321
  email,
@@ -12311,254 +12593,6 @@ const useAuth = () => {
12311
12593
  return context;
12312
12594
  };
12313
12595
 
12314
- /**
12315
- * Friendly error messages for common HTTP status codes.
12316
- */
12317
- const STATUS_MESSAGES = {
12318
- 400: 'Invalid request. Please check your input and try again.',
12319
- 401: 'Invalid credentials. Please check your email and password.',
12320
- 403: 'Access denied. You do not have permission to perform this action.',
12321
- 404: 'Account not found. Please check your email or create a new account.',
12322
- 409: 'This email is already registered.',
12323
- 429: 'Too many attempts. Please wait a moment and try again.',
12324
- };
12325
- /**
12326
- * Context-specific error messages for different auth operations.
12327
- */
12328
- const ERROR_CODE_MESSAGES = {
12329
- // 400 - Validation errors
12330
- 'MISSING_FIELDS': 'Email and password are required.',
12331
- 'MISSING_EMAIL': 'Email is required.',
12332
- 'MISSING_PASSWORD': 'Password is required.',
12333
- 'MISSING_TOKEN': 'Token is required.',
12334
- 'MISSING_PHONE_NUMBER': 'Phone number is required.',
12335
- 'MISSING_VERIFICATION_CODE': 'Phone number and verification code are required.',
12336
- 'MISSING_REDIRECT_URL': 'Redirect URL is required.',
12337
- 'MISSING_GOOGLE_TOKEN': 'Google token is required.',
12338
- 'INVALID_CLIENT_ID': 'Invalid client configuration.',
12339
- 'INVALID_REDIRECT_URL': 'Invalid redirect URL.',
12340
- 'INVALID_PHONE_NUMBER': 'Invalid phone number. Please check the format and try again.',
12341
- 'INVALID_GOOGLE_TOKEN': 'Invalid Google sign-in token.',
12342
- 'PASSWORD_TOO_SHORT': 'Password must be at least 8 characters long.',
12343
- 'PASSWORD_REQUIREMENTS_NOT_MET': 'New password must be at least 6 characters.',
12344
- 'EMAIL_ALREADY_VERIFIED': 'Your email is already verified.',
12345
- 'INVALID_CONFIRMATION': 'Please type DELETE to confirm account deletion.',
12346
- // 401 - Authentication errors
12347
- 'INVALID_CREDENTIALS': 'Invalid email or password.',
12348
- 'INCORRECT_PASSWORD': 'Current password is incorrect.',
12349
- 'INVALID_VERIFICATION_CODE': 'Invalid or expired verification code. Please try again.',
12350
- 'INVALID_TOKEN': 'This link has expired or is invalid. Please request a new one.',
12351
- 'TOKEN_EXPIRED': 'This link has expired. Please request a new one.',
12352
- 'TOKEN_ALREADY_USED': 'This link has already been used. Please request a new one.',
12353
- 'UNAUTHORIZED': 'You must be logged in to perform this action.',
12354
- 'GOOGLE_TOKEN_AUDIENCE_MISMATCH': 'Google sign-in failed. Token was not issued for this application.',
12355
- // 403 - Forbidden / verification required
12356
- 'EMAIL_NOT_VERIFIED': 'Please verify your email before signing in.',
12357
- 'ACCOUNT_LOCKED': 'Your account has been locked. Please verify your email to unlock.',
12358
- 'EMAIL_VERIFICATION_EXPIRED': 'Your verification deadline has passed and your account is locked. Please contact support.',
12359
- // 404
12360
- 'USER_NOT_FOUND': 'Account not found. Please check your email or create a new account.',
12361
- // 409 - Conflicts
12362
- 'EMAIL_ALREADY_EXISTS': 'This email is already registered.',
12363
- 'EMAIL_IN_USE': 'This email is already in use.',
12364
- // 429 - Rate limiting
12365
- 'RATE_LIMIT_EXCEEDED': 'Too many requests. Please try again later.',
12366
- 'TOO_MANY_MAGIC_LINKS': 'Too many magic link requests. Please try again later.',
12367
- 'TOO_MANY_VERIFICATION_ATTEMPTS': 'Too many verification attempts. Please wait and try again.',
12368
- 'MAX_VERIFICATION_ATTEMPTS': 'Maximum verification attempts reached. Please try again later.',
12369
- // 500 - Server errors
12370
- 'LOGIN_FAILED': 'Login failed. Please try again later.',
12371
- 'REGISTRATION_FAILED': 'Registration failed. Please try again later.',
12372
- 'GOOGLE_AUTH_NOT_CONFIGURED': 'Google sign-in is not available for this application.',
12373
- 'GOOGLE_AUTH_FAILED': 'Google sign-in failed. Please try again.',
12374
- 'GOOGLE_USERINFO_FAILED': 'Failed to retrieve your Google account information. Please try again.',
12375
- 'PHONE_VERIFICATION_FAILED': 'Phone verification failed. Please try again.',
12376
- 'SEND_VERIFICATION_CODE_FAILED': 'Failed to send verification code. Please try again.',
12377
- 'MAGIC_LINK_SEND_FAILED': 'Failed to send magic link. Please try again.',
12378
- 'MAGIC_LINK_VERIFICATION_FAILED': 'Magic link verification failed. Please try again.',
12379
- 'PASSWORD_RESET_FAILED': 'Failed to process password reset. Please try again.',
12380
- 'PASSWORD_RESET_COMPLETE_FAILED': 'Failed to reset password. Please try again.',
12381
- 'EMAIL_VERIFICATION_SEND_FAILED': 'Failed to send verification email. Please try again.',
12382
- 'EMAIL_VERIFICATION_FAILED': 'Email verification failed. Please try again.',
12383
- // Account management 500s
12384
- 'UPDATE_PROFILE_FAILED': 'Failed to update profile. Please try again.',
12385
- 'CHANGE_PASSWORD_FAILED': 'Failed to change password. Please try again.',
12386
- 'CHANGE_EMAIL_FAILED': 'Failed to change email. Please try again.',
12387
- 'UPDATE_PHONE_FAILED': 'Failed to update phone number. Please try again.',
12388
- 'DELETE_ACCOUNT_FAILED': 'Failed to delete account. Please try again.',
12389
- 'CONFIG_FETCH_FAILED': 'Failed to load configuration. Please try again.',
12390
- 'INTERNAL_ERROR': 'An unexpected error occurred. Please try again.',
12391
- // Legacy aliases (kept for backward compatibility)
12392
- 'INVALID_CODE': 'Invalid verification code. Please check and try again.',
12393
- 'CODE_EXPIRED': 'This code has expired. Please request a new one.',
12394
- 'PHONE_NOT_SUPPORTED': 'This phone number is not supported. Please try a different number.',
12395
- 'INVALID_PHONE': 'Invalid phone number. Please check the format and try again.',
12396
- 'PASSWORD_TOO_WEAK': 'Password is too weak. Please use at least 8 characters with a mix of letters and numbers.',
12397
- 'RATE_LIMITED': 'Too many attempts. Please wait a moment and try again.',
12398
- };
12399
- function isApiErrorLike(error) {
12400
- if (error instanceof SmartlinksApiError)
12401
- return true;
12402
- if (error && typeof error === 'object' && 'statusCode' in error && 'message' in error) {
12403
- const e = error;
12404
- return typeof e.statusCode === 'number' && typeof e.message === 'string';
12405
- }
12406
- return false;
12407
- }
12408
- /**
12409
- * Extracts a user-friendly error message from an error.
12410
- *
12411
- * Handles:
12412
- * - SmartlinksApiError (and duck-typed equivalents from proxy mode)
12413
- * - Standard Error: Uses message property
12414
- * - String: Passes through directly (for native bridge errors)
12415
- * - Unknown: Returns generic message
12416
- */
12417
- function getFriendlyErrorMessage(error) {
12418
- // Handle SmartlinksApiError or duck-typed API errors (proxy mode)
12419
- if (isApiErrorLike(error)) {
12420
- // First, check for specific error code (most precise)
12421
- const errorCode = error.errorCode || error.details?.errorCode || error.details?.error;
12422
- // For rate-limit errors, prefer the backend's message as it includes specific wait times
12423
- if (errorCode === 'RATE_LIMIT_EXCEEDED' && error.message && error.message !== '[object Object]') {
12424
- return error.message;
12425
- }
12426
- if (errorCode && ERROR_CODE_MESSAGES[errorCode]) {
12427
- return ERROR_CODE_MESSAGES[errorCode];
12428
- }
12429
- // Then, check status code for general category messages
12430
- if (error.statusCode >= 500) {
12431
- return 'Server error. Please try again later.';
12432
- }
12433
- // For 429 status, prefer backend message (may include wait time)
12434
- if (error.statusCode === 429 && error.message && error.message !== '[object Object]') {
12435
- return error.message;
12436
- }
12437
- if (STATUS_MESSAGES[error.statusCode]) {
12438
- return STATUS_MESSAGES[error.statusCode];
12439
- }
12440
- // Fall back to the server's message (already human-readable from backend)
12441
- return error.message;
12442
- }
12443
- // Handle standard Error objects
12444
- if (error instanceof Error) {
12445
- // SDK bug workaround: SDK may do `throw new Error(responseBodyObject)` which produces
12446
- // message "[object Object]". Check for API error properties attached to the Error instance.
12447
- const errAny = error;
12448
- // Check if the Error has API error properties directly attached (e.g., error.statusCode, error.errorCode)
12449
- if (typeof errAny.statusCode === 'number' || errAny.errorCode || errAny.response) {
12450
- // Try to extract from attached properties
12451
- const apiLike = errAny.response || errAny;
12452
- if (isApiErrorLike(apiLike)) {
12453
- return getFriendlyErrorMessage(apiLike);
12454
- }
12455
- }
12456
- // Check if the Error has a `cause` with API error details (modern Error cause pattern)
12457
- if (errAny.cause && typeof errAny.cause === 'object') {
12458
- if (isApiErrorLike(errAny.cause)) {
12459
- return getFriendlyErrorMessage(errAny.cause);
12460
- }
12461
- }
12462
- // If the message is "[object Object]", the error was constructed from a plain object
12463
- // This is useless - return a generic message instead
12464
- if (error.message === '[object Object]') {
12465
- // Log the actual error for debugging
12466
- console.warn('[AuthKit] Error with [object Object] message. Raw error:', JSON.stringify(errAny, Object.getOwnPropertyNames(errAny)));
12467
- return 'An unexpected error occurred. Please try again.';
12468
- }
12469
- // Check if the message itself contains a known API error pattern
12470
- if (/already (registered|exists)/i.test(error.message)) {
12471
- return 'This email is already registered.';
12472
- }
12473
- return error.message;
12474
- }
12475
- // Handle plain strings (e.g., from native bridge callbacks)
12476
- if (typeof error === 'string') {
12477
- return error;
12478
- }
12479
- // Unknown error type
12480
- return 'An unexpected error occurred. Please try again.';
12481
- }
12482
- /**
12483
- * Checks if an error represents a conflict (409) - typically duplicate registration.
12484
- */
12485
- function isConflictError(error) {
12486
- if (isApiErrorLike(error))
12487
- return error.statusCode === 409;
12488
- // Also check error message for keyword match (resilient fallback)
12489
- if (error instanceof Error && /already (registered|exists)/i.test(error.message))
12490
- return true;
12491
- return false;
12492
- }
12493
- /**
12494
- * Checks if an error represents invalid credentials (401).
12495
- */
12496
- function isAuthError(error) {
12497
- if (error instanceof SmartlinksApiError)
12498
- return error.isAuthError();
12499
- if (isApiErrorLike(error))
12500
- return error.statusCode === 401 || error.statusCode === 403;
12501
- return false;
12502
- }
12503
- /**
12504
- * Checks if an error represents rate limiting (429).
12505
- */
12506
- function isRateLimitError(error) {
12507
- if (error instanceof SmartlinksApiError)
12508
- return error.isRateLimited();
12509
- if (isApiErrorLike(error))
12510
- return error.statusCode === 429;
12511
- return false;
12512
- }
12513
- /**
12514
- * Checks if an error represents a server error (5xx).
12515
- */
12516
- function isServerError(error) {
12517
- if (error instanceof SmartlinksApiError)
12518
- return error.isServerError();
12519
- if (isApiErrorLike(error))
12520
- return error.statusCode >= 500;
12521
- return false;
12522
- }
12523
- /**
12524
- * Gets the HTTP status code from an error, if available.
12525
- */
12526
- function getErrorStatusCode(error) {
12527
- if (isApiErrorLike(error))
12528
- return error.statusCode;
12529
- return undefined;
12530
- }
12531
- /**
12532
- * Gets the server-specific error code from an error, if available.
12533
- */
12534
- function getErrorCode(error) {
12535
- if (isApiErrorLike(error)) {
12536
- return error.errorCode || error.details?.errorCode || error.details?.error;
12537
- }
12538
- return undefined;
12539
- }
12540
- /**
12541
- * Error codes that indicate the user needs to verify their email.
12542
- */
12543
- const EMAIL_VERIFICATION_ERROR_CODES = new Set([
12544
- 'EMAIL_NOT_VERIFIED',
12545
- 'ACCOUNT_LOCKED',
12546
- 'EMAIL_VERIFICATION_EXPIRED',
12547
- ]);
12548
- /**
12549
- * Checks if an error requires email verification action from the user.
12550
- */
12551
- function requiresEmailVerification(error) {
12552
- const code = getErrorCode(error);
12553
- if (code && EMAIL_VERIFICATION_ERROR_CODES.has(code))
12554
- return true;
12555
- // Also check the flag from the response body
12556
- if (error && typeof error === 'object' && 'requiresEmailVerification' in error) {
12557
- return error.requiresEmailVerification === true;
12558
- }
12559
- return false;
12560
- }
12561
-
12562
12596
  // VERSION: Update this when making changes to help identify which version is running
12563
12597
  const AUTH_UI_VERSION = '46';
12564
12598
  const LOG_PREFIX = `[SmartlinksAuthUI:v${AUTH_UI_VERSION}]`;
@@ -13852,11 +13886,13 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13852
13886
  setLoading(false);
13853
13887
  }
13854
13888
  };
13855
- const handlePhoneAuth = async (phoneNumber, verificationCode) => {
13889
+ const handlePhoneAuth = async (phoneNumber, verificationCode, displayName) => {
13856
13890
  setLoading(true);
13857
13891
  setError(undefined);
13858
13892
  try {
13859
13893
  if (!verificationCode) {
13894
+ // Phone verify endpoint handles account creation on the backend side,
13895
+ // so no need for ensureAccount here (register requires email which phone users may not have)
13860
13896
  // Send verification code via Twilio Verify Service
13861
13897
  await api.sendPhoneCode(phoneNumber);
13862
13898
  // Twilio Verify Service tracks the verification by phone number
@@ -13975,10 +14011,12 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13975
14011
  setLoading(false);
13976
14012
  }
13977
14013
  };
13978
- const handleMagicLink = async (email) => {
14014
+ const handleMagicLink = async (email, displayName) => {
13979
14015
  setLoading(true);
13980
14016
  setError(undefined);
13981
14017
  try {
14018
+ // Ensure account exists before sending magic link (creates if new, no-op if exists)
14019
+ await api.ensureAccount({ email, displayName });
13982
14020
  await api.sendMagicLink(email, getRedirectUrl());
13983
14021
  setAuthSuccess(true);
13984
14022
  setSuccessMessage('Magic link sent! Check your email to log in.');
@@ -14025,7 +14063,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
14025
14063
  ? 'hsl(var(--muted-foreground, 215 15% 45%))'
14026
14064
  : (resolvedTheme === 'dark' ? '#94a3b8' : '#6B7280'),
14027
14065
  fontSize: '0.875rem'
14028
- }, children: successMessage })] })) : mode === 'magic-link' ? (jsx(MagicLinkForm, { onSubmit: handleMagicLink, onCancel: () => setMode('login'), loading: loading, error: error })) : mode === 'phone' ? (jsx(PhoneAuthForm, { onSubmit: handlePhoneAuth, onBack: () => setMode('login'), loading: loading, error: error })) : mode === 'reset-password' ? (jsx(PasswordResetForm, { onSubmit: handlePasswordReset, onBack: () => {
14066
+ }, children: successMessage })] })) : mode === 'magic-link' ? (jsx(MagicLinkForm, { onSubmit: handleMagicLink, onCancel: () => setMode('login'), loading: loading, error: error, collectName: config?.collectNameOnPasswordlessSignup })) : mode === 'phone' ? (jsx(PhoneAuthForm, { onSubmit: handlePhoneAuth, onBack: () => setMode('login'), loading: loading, error: error, collectName: config?.collectNameOnPasswordlessSignup })) : mode === 'reset-password' ? (jsx(PasswordResetForm, { onSubmit: handlePasswordReset, onBack: () => {
14029
14067
  setMode('login');
14030
14068
  setResetSuccess(false);
14031
14069
  setResetToken(undefined); // Clear token when going back