@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
JavaScript
"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