UNPKG

@oxyhq/services

Version:

Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀

665 lines (630 loc) • 21.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = _interopRequireWildcard(require("react")); var _reactNative = require("react-native"); var _vectorIcons = require("@expo/vector-icons"); var _fonts = require("../../styles/fonts"); var _jsxRuntime = require("react/jsx-runtime"); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } // Color palette for different states const colorPalette = { primary: { main: '#d169e5', light: '#e8b5f0', dark: '#a64db3' }, secondary: { main: '#666666', light: '#999999', dark: '#333333' }, error: { main: '#D32F2F', light: '#ffcdd2', dark: '#b71c1c' }, success: { main: '#2E7D32', light: '#c8e6c9', dark: '#1b5e20' }, warning: { main: '#FF9800', light: '#ffe0b2', dark: '#e65100' } }; // Surface scale for consistent theming const surfaceScale = level => { const base = 255; const value = Math.round(base - level * 255); return `#${value.toString(16).padStart(2, '0').repeat(3)}`; }; // Password strength calculation const calculatePasswordStrength = password => { if (!password) return { score: 0, feedback: '', color: '#E0E0E0', label: '' }; let score = 0; const feedback = []; if (password.length >= 8) score += 25;else feedback.push('At least 8 characters'); if (/[A-Z]/.test(password)) score += 25;else feedback.push('One uppercase letter'); if (/[a-z]/.test(password)) score += 25;else feedback.push('One lowercase letter'); if (/[\d\W]/.test(password)) score += 25;else feedback.push('One number or special character'); const colors = { 0: '#E0E0E0', 25: '#D32F2F', 50: '#FF9800', 75: '#2196F3', 100: '#4CAF50' }; const labels = { 0: '', 25: 'Weak', 50: 'Fair', 75: 'Good', 100: 'Strong' }; return { score, feedback: score === 100 ? 'Strong password!' : `Missing: ${feedback.join(', ')}`, color: colors[score] || colors[0], label: labels[score] || '' }; }; // Input formatting utilities const formatters = { phone: value => { const cleaned = value.replace(/\D/g, ''); const match = cleaned.match(/^(\d{3})(\d{3})(\d{4})$/); if (match) return `(${match[1]}) ${match[2]}-${match[3]}`; return value; }, creditCard: value => { const cleaned = value.replace(/\D/g, ''); const match = cleaned.match(/^(\d{4})(\d{4})(\d{4})(\d{4})$/); if (match) return `${match[1]} ${match[2]} ${match[3]} ${match[4]}`; return value.replace(/(.{4})/g, '$1 ').trim(); }, currency: value => { const cleaned = value.replace(/[^\d.]/g, ''); const num = Number.parseFloat(cleaned); return isNaN(num) ? value : `$${num.toFixed(2)}`; } }; // Debounce hook const useDebounce = (value, delay) => { const [debouncedValue, setDebouncedValue] = (0, _react.useState)(value); (0, _react.useEffect)(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(handler); }; }, [value, delay]); return debouncedValue; }; const TextField = /*#__PURE__*/(0, _react.forwardRef)(({ // Basic props label, variant = 'filled', color = 'primary', // Leading and trailing leading, trailing, // States error, success = false, loading = false, disabled = false, // Helper text helperText, // Enhanced features maxLength, showCharacterCount, inputMask, customMask, formatValue, validateOnChange, debounceMs = 300, passwordStrength = false, clearable = false, // Mouse events onMouseEnter, onMouseLeave, // Styling style, inputContainerStyle, inputStyle, leadingContainerStyle, trailingContainerStyle, // Callbacks onValidationChange, onClear, // TextInput props placeholder, onFocus, onBlur, onChangeText, value = '', secureTextEntry, ...rest }, ref) => { // State management const [focused, setFocused] = (0, _react.useState)(false); const [hovered, setHovered] = (0, _react.useState)(false); const [showPassword, setShowPassword] = (0, _react.useState)(false); const [internalValue, setInternalValue] = (0, _react.useState)(value); const [isValidating, setIsValidating] = (0, _react.useState)(false); // Refs const focusAnimation = (0, _react.useRef)(new _reactNative.Animated.Value(0)).current; const activeAnimation = (0, _react.useRef)(new _reactNative.Animated.Value(Boolean(value) ? 1 : 0)).current; const inputRef = (0, _react.useRef)(null); // Get color palette const palette = colorPalette[color] || colorPalette.primary; // Determine if we should show error colors const effectiveColor = error ? 'error' : success ? 'success' : color; const effectivePalette = colorPalette[effectiveColor] || colorPalette.primary; // Get icon color based on focus state and error state const iconColor = error ? effectivePalette.main // Always show error color when there's an error : focused ? effectivePalette.main : surfaceScale(0.62); // Helper function to clone React elements with updated color // React 19: ref is now a regular prop, accessed via element.props.ref const cloneWithColor = (element, color) => { if (/*#__PURE__*/_react.default.isValidElement(element) && element.type) { // React 19: ref is now in props, so we spread all props and override color const elementProps = element.props || {}; return /*#__PURE__*/_react.default.cloneElement(element, { ...elementProps, color }); } return element; }; // Render leading/trailing elements const leadingNode = typeof leading === 'function' ? leading({ color: iconColor, size: 24 }) : cloneWithColor(leading, iconColor); const trailingNode = typeof trailing === 'function' ? trailing({ color: iconColor, size: 24 }) : cloneWithColor(trailing, iconColor); // Debounced value for validation const debouncedValue = useDebounce(internalValue, debounceMs); // Password strength calculation const passwordStrengthData = (0, _react.useMemo)(() => { if (passwordStrength && secureTextEntry && internalValue) { return calculatePasswordStrength(internalValue); } return null; }, [passwordStrength, secureTextEntry, internalValue]); // Format input value const formatInputValue = (0, _react.useCallback)(text => { if (formatValue) return formatValue(text); if (inputMask && inputMask !== 'custom' && formatters[inputMask]) { return formatters[inputMask](text); } if (customMask) return customMask(text); return text; }, [formatValue, inputMask, customMask]); // Handle focus const handleFocus = (0, _react.useCallback)(event => { if (disabled) return; setFocused(true); // Convert FocusEvent to the expected type for parent callback const syntheticEvent = event; onFocus?.(syntheticEvent); }, [disabled, onFocus]); // Handle blur const handleBlur = (0, _react.useCallback)(event => { setFocused(false); // Convert BlurEvent to the expected type for parent callback const syntheticEvent = event; onBlur?.(syntheticEvent); }, [onBlur]); // Handle mouse events const handleMouseEnter = (0, _react.useCallback)(event => { onMouseEnter?.(event); setHovered(true); }, [onMouseEnter]); const handleMouseLeave = (0, _react.useCallback)(event => { onMouseLeave?.(event); setHovered(false); }, [onMouseLeave]); // Handle text change const handleChangeText = (0, _react.useCallback)(text => { const formattedText = formatInputValue(text); setInternalValue(formattedText); onChangeText?.(formattedText); }, [formatInputValue, onChangeText]); // Handle clear const handleClear = (0, _react.useCallback)(() => { setInternalValue(''); onChangeText?.(''); onClear?.(); inputRef.current?.focus(); }, [onChangeText, onClear]); // Toggle password visibility const togglePasswordVisibility = (0, _react.useCallback)(() => { setShowPassword(prev => !prev); }, []); // Animate focus state (0, _react.useEffect)(() => { _reactNative.Animated.timing(focusAnimation, { toValue: focused ? 1 : 0, duration: 200, useNativeDriver: false }).start(); }, [focused, focusAnimation]); // Animate active state (when focused or has value) (0, _react.useEffect)(() => { const shouldBeActive = focused || Boolean(internalValue); _reactNative.Animated.timing(activeAnimation, { toValue: shouldBeActive ? 1 : 0, duration: 200, useNativeDriver: false }).start(); }, [focused, internalValue, activeAnimation]); // Validation effect (0, _react.useEffect)(() => { if (!validateOnChange || !onValidationChange) return; const timer = setTimeout(() => { setIsValidating(true); const isValid = !error && debouncedValue.length > 0; onValidationChange(isValid, debouncedValue); setIsValidating(false); }, 100); return () => clearTimeout(timer); }, [debouncedValue, validateOnChange, onValidationChange, error]); // Update internal value when prop changes (0, _react.useEffect)(() => { setInternalValue(value); }, [value]); // Styles const styles = (0, _react.useMemo)(() => { const isActive = focused || Boolean(internalValue); return _reactNative.StyleSheet.create({ container: { width: '100%', marginBottom: 24 }, inputContainer: { flexDirection: 'row', alignItems: 'center', minHeight: variant === 'standard' ? 48 : 56, backgroundColor: variant === 'filled' ? focused ? surfaceScale(0.08) : hovered ? surfaceScale(0.08) : surfaceScale(0.04) : 'transparent', borderRadius: variant === 'standard' ? 0 : variant === 'filled' ? 16 : 8, borderTopLeftRadius: variant === 'filled' ? 16 : undefined, borderTopRightRadius: variant === 'filled' ? 16 : undefined, borderBottomLeftRadius: variant === 'filled' ? 0 : undefined, borderBottomRightRadius: variant === 'filled' ? 0 : undefined, borderWidth: variant === 'outlined' ? focused ? 2 : 1 : 0, borderColor: error ? effectivePalette.main // Always show error color when there's an error : focused ? effectivePalette.main : hovered ? surfaceScale(0.87) : surfaceScale(0.42), position: 'relative', ..._reactNative.Platform.select({ web: { outlineStyle: undefined, outlineWidth: 0, outlineOffset: 0 }, default: {} }) }, input: { flex: 1, minHeight: variant === 'standard' ? 48 : 56, paddingStart: leadingNode ? 12 : variant === 'standard' ? 0 : 16, paddingEnd: trailingNode || clearable || secureTextEntry ? 12 : variant === 'standard' ? 0 : 16, paddingTop: variant === 'filled' && label ? 18 : 0, color: surfaceScale(0.87), fontSize: 16, borderWidth: 0, backgroundColor: 'transparent', ..._reactNative.Platform.select({ web: { border: 'none', outlineStyle: undefined, outlineWidth: 0, outlineOffset: 0, boxShadow: 'none', '-webkit-appearance': 'none', '-moz-appearance': 'none', appearance: 'none' }, default: {} }) }, leading: { justifyContent: 'center', alignItems: 'center', width: 24, height: 24, marginStart: variant === 'standard' ? 0 : 12, marginVertical: variant === 'standard' ? 12 : 16 }, trailing: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginEnd: variant === 'standard' ? 0 : 12, marginVertical: variant === 'standard' ? 12 : 16 }, underline: { position: 'absolute', start: 0, end: 0, bottom: 0, height: 1, backgroundColor: error ? effectivePalette.main // Always show error color when there's an error : hovered ? surfaceScale(0.87) : surfaceScale(0.42) }, underlineFocused: { position: 'absolute', start: 0, end: 0, bottom: 0, height: 2, backgroundColor: effectivePalette.main }, labelContainer: { justifyContent: 'center', position: 'absolute', top: 0, start: variant === 'standard' ? leadingNode ? 36 : 0 : leadingNode ? 48 : 16, height: variant === 'standard' ? 48 : 56 }, label: { fontSize: 16, fontFamily: _fonts.fontFamilies.phuduSemiBold, color: surfaceScale(0.87) }, helperText: { fontSize: 12, fontFamily: _fonts.fontFamilies.phuduMedium, marginTop: 4, marginHorizontal: 16, color: surfaceScale(0.6) }, passwordStrengthContainer: { marginTop: 8, marginHorizontal: 16 }, passwordStrengthBar: { height: 4, backgroundColor: '#E0E0E0', borderRadius: 2, overflow: 'hidden' }, passwordStrengthFill: { height: '100%', borderRadius: 2 }, passwordStrengthText: { fontSize: 11, fontWeight: '600', marginTop: 4 }, characterCount: { fontSize: 11, marginTop: 4, marginHorizontal: 16, textAlign: 'right', color: surfaceScale(0.6) }, clearButton: { padding: 4, marginLeft: 8 }, passwordToggle: { padding: 4, marginLeft: 8 }, validationIndicator: { marginLeft: 8 } }); }, [variant, focused, hovered, effectivePalette, leadingNode, trailingNode, clearable, secureTextEntry, label, error, internalValue]); // Character count display const characterCount = internalValue.length; const showCount = showCharacterCount && maxLength; // Render password strength indicator const renderPasswordStrength = () => { if (!passwordStrengthData) return null; return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.passwordStrengthContainer, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.passwordStrengthBar, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [styles.passwordStrengthFill, { width: `${passwordStrengthData.score}%`, backgroundColor: passwordStrengthData.color }] }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.passwordStrengthText, { color: passwordStrengthData.color }], children: passwordStrengthData.label })] }); }; // Render character count const renderCharacterCount = () => { if (!showCount) return null; return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, { style: styles.characterCount, children: [characterCount, "/", maxLength] }); }; // Get helper text content (error takes precedence over helper text) const helperTextContent = error || helperText; // Render trailing elements const renderTrailingElements = () => { const elements = []; // Loading indicator if (isValidating) { elements.push(/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, { size: "small", color: effectivePalette.main, style: styles.validationIndicator }, "validating")); } // Loading indicator if (loading && !isValidating) { elements.push(/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, { size: "small", color: effectivePalette.main, style: styles.validationIndicator }, "loading")); } // Success indicator if (success && !loading && !isValidating) { elements.push(/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "checkmark-circle", size: 22, color: colorPalette.success.main, style: styles.validationIndicator }, "success")); } // Clear button if (clearable && internalValue && !secureTextEntry) { elements.push(/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: styles.clearButton, onPress: handleClear, hitSlop: { top: 10, bottom: 10, left: 10, right: 10 }, accessibilityLabel: "Clear input", accessibilityRole: "button", children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "close-circle", size: 20, color: iconColor }) }, "clear")); } // Password toggle if (secureTextEntry) { elements.push(/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: styles.passwordToggle, onPress: togglePasswordVisibility, hitSlop: { top: 10, bottom: 10, left: 10, right: 10 }, accessibilityLabel: showPassword ? "Hide password" : "Show password", accessibilityRole: "button", children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: showPassword ? "eye-off" : "eye", size: 22, color: iconColor }) }, "password")); } return elements; }; return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: [styles.container, style], ...(_reactNative.Platform.OS === 'web' && { className: 'oxy-textfield-container' }), children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: [styles.inputContainer, inputContainerStyle], ...(_reactNative.Platform.OS === 'web' && { onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave }), children: [leadingNode && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [styles.leading, leadingContainerStyle], children: leadingNode }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TextInput, { ref: r => { if (typeof ref === 'function') { ref(r); } else if (ref && typeof ref === 'object') { // @ts-ignore - React ref assignment ref.current = r; } // @ts-ignore - Internal ref assignment inputRef.current = r; }, style: [styles.input, inputStyle], placeholder: label ? focused ? placeholder : undefined : placeholder, placeholderTextColor: surfaceScale(0.4), selectionColor: effectivePalette.main, onFocus: handleFocus, onBlur: handleBlur, onChangeText: handleChangeText, secureTextEntry: secureTextEntry && !showPassword, value: internalValue, editable: !disabled, maxLength: maxLength, ...(_reactNative.Platform.OS === 'web' && { className: 'oxy-textfield-input' }), ...rest }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: [styles.trailing, trailingContainerStyle], children: [trailingNode, renderTrailingElements()] }), (variant === 'filled' || variant === 'standard') && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [styles.underline, { pointerEvents: 'none' }] }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, { style: [styles.underlineFocused, { transform: [{ scaleX: focusAnimation }], pointerEvents: 'none' }] })] }), label && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [styles.labelContainer, { pointerEvents: 'none' }], children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.Text, { style: [styles.label, { color: focusAnimation.interpolate({ inputRange: [0, 1], outputRange: [error ? effectivePalette.main : surfaceScale(0.87), effectivePalette.main] }), fontSize: activeAnimation.interpolate({ inputRange: [0, 1], outputRange: [16, 12] }), transform: [{ translateY: activeAnimation.interpolate({ inputRange: [0, 1], outputRange: [0, variant === 'filled' ? -12 : variant === 'outlined' ? -28 : -24] }) }] }], children: label }) })] }), helperTextContent && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.helperText, error && { color: effectivePalette.main }], children: helperTextContent }), renderPasswordStrength(), renderCharacterCount()] }); }); TextField.displayName = 'TextField'; var _default = exports.default = TextField; //# sourceMappingURL=TextField.js.map