@widergy/mobile-ui 2.9.7 → 2.11.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [2.11.0](https://github.com/widergy/mobile-ui/compare/v2.10.0...v2.11.0) (2026-04-13)
2
+
3
+
4
+ ### Features
5
+
6
+ * [UGGC-95] UTCoupon ([#493](https://github.com/widergy/mobile-ui/issues/493)) ([11a3367](https://github.com/widergy/mobile-ui/commit/11a336717acfe31e146288981360481b5ad73268))
7
+
8
+ # [2.10.0](https://github.com/widergy/mobile-ui/compare/v2.9.7...v2.10.0) (2026-04-13)
9
+
10
+
11
+ ### Features
12
+
13
+ * [PROMI-221] UTPhoneInput soporta valor string u objeto con prefix editable ([#496](https://github.com/widergy/mobile-ui/issues/496)) ([07a8a8b](https://github.com/widergy/mobile-ui/commit/07a8a8bf4101efcd89176119de7a8aa62252f79e))
14
+
1
15
  ## [2.9.7](https://github.com/widergy/mobile-ui/compare/v2.9.6...v2.9.7) (2026-04-10)
2
16
 
3
17
 
@@ -0,0 +1,69 @@
1
+ # UTCoupon
2
+
3
+ Tarjeta de cupón seleccionable usada en flujos de pedidos. Muestra imagen de marca, nombre del producto, condiciones de uso, fecha de vencimiento y precio. Soporta tres estados: default, activo y agotado.
4
+
5
+ La forma de ticket (recortes elípticos en los bordes laterales) se implementa con SVG.
6
+
7
+ ## Props
8
+
9
+ | Prop | Tipo | Default | Descripción |
10
+ | ------------ | --------------------------- | ------- | ------------------------------------------------------------------------------------------ |
11
+ | `image` | `number \| { uri: string }` | — | Fuente de imagen para la sección izquierda (misma forma que `Image` source de RN) |
12
+ | `detail` | `string` | — | Texto pequeño superior (ej. municipio, código de producto) |
13
+ | `title` | `string` | — | Texto principal central (ej. "Carga 11Kg") |
14
+ | `subtitle` | `string` | — | Texto secundario inferior (ej. "1 uso mensualmente") |
15
+ | `value` | `string` | — | Texto de la sección derecha (ej. "$2.500") |
16
+ | `active` | `bool` | `false` | Estado seleccionado: borde azul. Mutuamente excluyente con `disabled`. |
17
+ | `disabled` | `bool` | `false` | Estado agotado: overlay gris, `onPress` deshabilitado. Mutuamente excluyente con `active`. |
18
+ | `onPress` | `func` | — | Callback de selección. Se ignora si `disabled` es `true`. |
19
+ | `style` | `shape` | `{}` | Overrides de estilo por subelemento (ver tabla de llaves). |
20
+ | `dataTestId` | `string` | — | ID raíz; los subelementos añaden sufijos automáticamente. |
21
+
22
+ ## Uso
23
+
24
+ ```jsx
25
+ import { UTCoupon } from '@widergy/mobile-ui';
26
+
27
+ // Default
28
+ <UTCoupon
29
+ image={require('./assets/logo.png')}
30
+ detail="Mun. Las Condes"
31
+ title="Carga 11Kg"
32
+ subtitle="1 uso mensualmente"
33
+ value="$2.500"
34
+ onPress={() => handleSelect(coupon)}
35
+ />
36
+
37
+ // Activo (seleccionado)
38
+ <UTCoupon
39
+ image={{ uri: 'https://example.com/logo.png' }}
40
+ detail="ANM51-1000220101-3"
41
+ title="Carga 11Kg"
42
+ subtitle="Vence el 16/08/2025"
43
+ value="$2.500"
44
+ active
45
+ onPress={() => handleSelect(coupon)}
46
+ />
47
+
48
+ // Agotado
49
+ <UTCoupon
50
+ image={require('./assets/logo.png')}
51
+ title="Carga 11Kg"
52
+ subtitle="Sin stock"
53
+ value="$2.500"
54
+ disabled
55
+ />
56
+ ```
57
+
58
+ ## Test IDs
59
+
60
+ Dado `dataTestId="coupon"`, los subelementos reciben:
61
+
62
+ | Subelemento | testID |
63
+ | ----------- | ---------------------- |
64
+ | Raíz | `"coupon"` |
65
+ | Imagen | `"coupon.icon"` |
66
+ | Detail | `"coupon.description"` |
67
+ | Title | `"coupon.titleText"` |
68
+ | Subtitle | `"coupon.subtitle"` |
69
+ | Value | `"coupon.label"` |
@@ -0,0 +1,8 @@
1
+ export const BORDER_RADIUS = 12;
2
+
3
+ export const ICON_SECTION_WIDTH = 80;
4
+
5
+ // Notch (elliptical cutout at top/bottom edges, centered near the icon section right edge)
6
+ export const NOTCH_RADIUS_X = 26; // horizontal semi-axis (notch width = 52)
7
+ export const NOTCH_RADIUS_Y = 8; // vertical semi-axis (notch depth from edge = 8)
8
+ export const NOTCH_CENTER_X = 104; // fixed x position of the notch center
@@ -0,0 +1,159 @@
1
+ import React, { memo, useRef, useState } from 'react';
2
+ import { Image, TouchableOpacity, View } from 'react-native';
3
+ import { ClipPath, Defs, Path, Rect, Svg } from 'react-native-svg';
4
+
5
+ import UTLabel from '../UTLabel';
6
+ import { useTheme } from '../../theming';
7
+ import { TEST_ID_CONSTANTS } from '../../constants/testIds';
8
+
9
+ import { defaultProps, propTypes } from './proptypes';
10
+ import { retrieveStyle } from './theme';
11
+ import { ICON_SECTION_WIDTH } from './constants';
12
+ import { buildCouponPath } from './utils';
13
+
14
+ const {
15
+ description: detailTestId,
16
+ icon: iconTestId,
17
+ label: valueTestId,
18
+ subtitle: subtitleTestId,
19
+ titleText: titleTestId
20
+ } = TEST_ID_CONSTANTS;
21
+
22
+ const UTCoupon = ({
23
+ active = false,
24
+ dataTestId,
25
+ detail,
26
+ disabled = false,
27
+ image,
28
+ onPress,
29
+ style,
30
+ subtitle,
31
+ title,
32
+ value
33
+ }) => {
34
+ const theme = useTheme();
35
+ const themedStyles = retrieveStyle({ theme, style });
36
+ const [cardLayout, setCardLayout] = useState({ width: 0, height: 0 });
37
+ const clipId = useRef(`clip${Math.random()}`).current;
38
+
39
+ const handleLayout = ({ nativeEvent: { layout } }) => {
40
+ if (layout.width !== cardLayout.width || layout.height !== cardLayout.height) {
41
+ setCardLayout({ width: layout.width, height: layout.height });
42
+ }
43
+ };
44
+
45
+ const couponPath = cardLayout.width > 0 ? buildCouponPath(cardLayout.width, cardLayout.height) : null;
46
+
47
+ const couponBorderPath =
48
+ active && cardLayout.width > 0 ? buildCouponPath(cardLayout.width, cardLayout.height, 1) : null; // Border path inset by 1px (= strokeWidth/2) so the full 2px stroke renders
49
+
50
+ return (
51
+ <View testID={dataTestId} style={themedStyles.root} onLayout={handleLayout}>
52
+ {couponPath && (
53
+ <Svg width={cardLayout.width} height={cardLayout.height} style={themedStyles.absoluteFill}>
54
+ <Defs>
55
+ <ClipPath id={clipId}>
56
+ <Path d={couponPath} />
57
+ </ClipPath>
58
+ </Defs>
59
+ <Path d={couponPath} fill={theme.Palette.light['01']} />
60
+ <Rect
61
+ x={0}
62
+ y={0}
63
+ width={ICON_SECTION_WIDTH}
64
+ height={cardLayout.height}
65
+ fill={theme.Palette.information['01']}
66
+ clipPath={`url(#${clipId})`}
67
+ />
68
+ </Svg>
69
+ )}
70
+
71
+ <TouchableOpacity
72
+ style={themedStyles.contentRow}
73
+ onPress={disabled ? undefined : onPress}
74
+ activeOpacity={disabled ? 1 : 0.7}
75
+ >
76
+ <View style={themedStyles.iconContainer}>
77
+ {image && (
78
+ <Image
79
+ testID={dataTestId ? `${dataTestId}.${iconTestId}` : undefined}
80
+ source={image}
81
+ style={[themedStyles.image, { height: cardLayout.height }]}
82
+ />
83
+ )}
84
+ </View>
85
+
86
+ <View style={themedStyles.rightSection}>
87
+ <View style={themedStyles.textContainer}>
88
+ {detail && (
89
+ <UTLabel
90
+ dataTestId={dataTestId ? `${dataTestId}.${detailTestId}` : undefined}
91
+ colorTheme="gray"
92
+ variant="xsmall"
93
+ style={themedStyles.detail}
94
+ >
95
+ {detail}
96
+ </UTLabel>
97
+ )}
98
+ {title && (
99
+ <UTLabel
100
+ dataTestId={dataTestId ? `${dataTestId}.${titleTestId}` : undefined}
101
+ colorTheme="dark"
102
+ variant="title2"
103
+ weight="medium"
104
+ style={themedStyles.title}
105
+ >
106
+ {title}
107
+ </UTLabel>
108
+ )}
109
+ {subtitle && (
110
+ <UTLabel
111
+ dataTestId={dataTestId ? `${dataTestId}.${subtitleTestId}` : undefined}
112
+ colorTheme="gray"
113
+ variant="small"
114
+ style={themedStyles.subtitle}
115
+ >
116
+ {subtitle}
117
+ </UTLabel>
118
+ )}
119
+ </View>
120
+
121
+ {value && (
122
+ <UTLabel
123
+ dataTestId={dataTestId ? `${dataTestId}.${valueTestId}` : undefined}
124
+ colorTheme="accent"
125
+ variant="title2"
126
+ weight="medium"
127
+ style={themedStyles.value}
128
+ >
129
+ {value}
130
+ </UTLabel>
131
+ )}
132
+ </View>
133
+ </TouchableOpacity>
134
+
135
+ {disabled && (
136
+ <View style={[themedStyles.absoluteFill, themedStyles.disabledOverlay]} pointerEvents="none" />
137
+ )}
138
+
139
+ {couponBorderPath && (
140
+ <View style={themedStyles.absoluteFill} pointerEvents="none">
141
+ <Svg width={cardLayout.width} height={cardLayout.height} style={themedStyles.absoluteFill}>
142
+ <Path
143
+ d={couponBorderPath}
144
+ fill="none"
145
+ stroke={theme.Palette.accent['05']}
146
+ strokeWidth={2}
147
+ strokeLinejoin="round"
148
+ />
149
+ </Svg>
150
+ </View>
151
+ )}
152
+ </View>
153
+ );
154
+ };
155
+
156
+ UTCoupon.propTypes = propTypes;
157
+ UTCoupon.defaultProps = defaultProps;
158
+
159
+ export default memo(UTCoupon);
@@ -0,0 +1,29 @@
1
+ import { bool, func, number, object, oneOfType, shape, string } from 'prop-types';
2
+
3
+ export const propTypes = {
4
+ image: oneOfType([number, shape({ uri: string })]),
5
+ detail: string,
6
+ title: string,
7
+ subtitle: string,
8
+ value: string,
9
+ active: bool,
10
+ disabled: bool,
11
+ onPress: func,
12
+ dataTestId: string,
13
+ style: shape({
14
+ root: object,
15
+ iconContainer: object,
16
+ image: object,
17
+ textContainer: object,
18
+ detail: object,
19
+ title: object,
20
+ subtitle: object,
21
+ value: object
22
+ })
23
+ };
24
+
25
+ export const defaultProps = {
26
+ active: false,
27
+ disabled: false,
28
+ style: {}
29
+ };
@@ -0,0 +1,71 @@
1
+ import { getShadow, mergeMultipleStyles } from '../../utils/styleUtils';
2
+
3
+ import { BORDER_RADIUS, ICON_SECTION_WIDTH } from './constants';
4
+
5
+ const ownStyles = {
6
+ root: {
7
+ width: '100%'
8
+ },
9
+ absoluteFill: {
10
+ position: 'absolute',
11
+ left: 0,
12
+ right: 0,
13
+ top: 0,
14
+ bottom: 0
15
+ },
16
+ contentRow: {
17
+ flexDirection: 'row',
18
+ alignItems: 'stretch'
19
+ },
20
+ iconContainer: {
21
+ width: ICON_SECTION_WIDTH,
22
+ alignSelf: 'stretch',
23
+ alignItems: 'center',
24
+ justifyContent: 'center',
25
+ borderTopLeftRadius: BORDER_RADIUS,
26
+ borderBottomLeftRadius: BORDER_RADIUS,
27
+ overflow: 'hidden'
28
+ },
29
+ image: {
30
+ position: 'absolute',
31
+ top: 0,
32
+ left: 0,
33
+ width: ICON_SECTION_WIDTH,
34
+ resizeMode: 'cover'
35
+ },
36
+ rightSection: {
37
+ flex: 1,
38
+ flexDirection: 'row',
39
+ alignItems: 'center',
40
+ paddingVertical: 18,
41
+ paddingLeft: 14,
42
+ paddingRight: 14
43
+ },
44
+ textContainer: {
45
+ flex: 1,
46
+ gap: 4
47
+ },
48
+ detail: {},
49
+ title: {},
50
+ subtitle: {},
51
+ value: {
52
+ marginLeft: 8,
53
+ textAlign: 'right'
54
+ },
55
+ disabledOverlay: {
56
+ borderRadius: BORDER_RADIUS
57
+ }
58
+ };
59
+
60
+ const getThemeStyles = theme => ({
61
+ root: {
62
+ borderRadius: BORDER_RADIUS * 2,
63
+ ...getShadow({ level: 1, theme })
64
+ },
65
+ disabledOverlay: {
66
+ backgroundColor: theme.colors.effect
67
+ }
68
+ });
69
+
70
+ export const retrieveStyle = ({ theme, style = {} }) =>
71
+ mergeMultipleStyles(ownStyles, getThemeStyles(theme), theme?.UTCoupon, style);
@@ -0,0 +1,40 @@
1
+ import { BORDER_RADIUS, NOTCH_CENTER_X, NOTCH_RADIUS_X, NOTCH_RADIUS_Y } from './constants';
2
+
3
+ // Traces a rounded rectangle with two concave half-ellipse notches on the top and bottom edges, centered at x = NOTCH_CENTER_X.
4
+ // `inset` shifts all edges inward by this many pixels — use strokeWidth/2 so the full stroke renders within the SVG viewport without clipping.
5
+ export const buildCouponPath = (width, height, inset = 0) => {
6
+ // KAPPA = 4*(√2−1)/3 ≈ 0.5523: the control-point ratio that best fits a cubic bezier to a quarter-ellipse arc (minimises radial error at t=0.5).
7
+ const KAPPA = 0.5523;
8
+
9
+ const cornerR = BORDER_RADIUS - inset; // effective corner radius after inset
10
+ const notchX = NOTCH_CENTER_X; // x position of the notch center
11
+ const notchRx = NOTCH_RADIUS_X; // notch horizontal semi-axis (width)
12
+ const notchRy = NOTCH_RADIUS_Y; // notch vertical semi-axis (depth)
13
+ const notchCp = notchRx * KAPPA; // bezier control-point offset for notch curves
14
+ const cornerCp = cornerR * KAPPA; // bezier control-point offset for corner curves
15
+
16
+ return [
17
+ `M ${cornerR + inset} ${inset}`,
18
+ `L ${notchX - notchRx} ${inset}`,
19
+ // Top notch: two beziers forming a half-ellipse curving DOWNWARD into the card.
20
+ `C ${notchX - notchRx + notchCp} ${inset} ${notchX - notchCp} ${inset + notchRy} ${notchX} ${inset + notchRy}`,
21
+ `C ${notchX + notchCp} ${inset + notchRy} ${notchX + notchRx - notchCp} ${inset} ${notchX + notchRx} ${inset}`,
22
+ `L ${width - cornerR - inset} ${inset}`,
23
+ // Top-right corner
24
+ `C ${width - cornerR - inset + cornerCp} ${inset} ${width - inset} ${inset + cornerCp} ${width - inset} ${cornerR + inset}`,
25
+ `L ${width - inset} ${height - cornerR - inset}`,
26
+ // Bottom-right corner
27
+ `C ${width - inset} ${height - cornerR - inset + cornerCp} ${width - cornerR - inset + cornerCp} ${height - inset} ${width - cornerR - inset} ${height - inset}`,
28
+ `L ${notchX + notchRx} ${height - inset}`,
29
+ // Bottom notch: two beziers forming a half-ellipse curving UPWARD into the card.
30
+ `C ${notchX + notchRx - notchCp} ${height - inset} ${notchX + notchCp} ${height - inset - notchRy} ${notchX} ${height - inset - notchRy}`,
31
+ `C ${notchX - notchCp} ${height - inset - notchRy} ${notchX - notchRx + notchCp} ${height - inset} ${notchX - notchRx} ${height - inset}`,
32
+ `L ${cornerR + inset} ${height - inset}`,
33
+ // Bottom-left corner
34
+ `C ${cornerR + inset - cornerCp} ${height - inset} ${inset} ${height - cornerR - inset + cornerCp} ${inset} ${height - cornerR - inset}`,
35
+ `L ${inset} ${cornerR + inset}`,
36
+ // Top-left corner
37
+ `C ${inset} ${cornerR + inset - cornerCp} ${cornerR + inset - cornerCp} ${inset} ${cornerR + inset} ${inset}`,
38
+ 'Z'
39
+ ].join(' ');
40
+ };
@@ -1,6 +1,6 @@
1
1
  import React, { PureComponent, createRef } from 'react';
2
2
  import { View } from 'react-native';
3
- import { bool, elementType, func, number, shape, string } from 'prop-types';
3
+ import { bool, elementType, func, number, oneOfType, shape, string } from 'prop-types';
4
4
 
5
5
  import { COMPONENT_KEYS } from '../UTBaseInputField/constants';
6
6
  import { formatErrorToValidation } from '../UTValidation/utils';
@@ -53,9 +53,11 @@ class UTPhoneInput extends PureComponent {
53
53
 
54
54
  changeText = updateFunction => {
55
55
  const { areaCode, phoneNumber } = this.state;
56
- const { withAreaCode } = this.props;
56
+ const { withAreaCode, prefix, editablePrefix } = this.props;
57
57
 
58
- if (withAreaCode && (areaCode || phoneNumber)) {
58
+ if (prefix !== undefined && editablePrefix) {
59
+ updateFunction({ number: phoneNumber, prefix });
60
+ } else if (withAreaCode && (areaCode || phoneNumber)) {
59
61
  updateFunction(`${areaCode}-${phoneNumber}`);
60
62
  } else if (!withAreaCode) {
61
63
  updateFunction(phoneNumber);
@@ -137,10 +139,14 @@ class UTPhoneInput extends PureComponent {
137
139
  };
138
140
 
139
141
  updateValue = () => {
140
- const { value, withAreaCode } = this.props;
142
+ const { value, withAreaCode, prefix, editablePrefix, onChange } = this.props;
141
143
  if (value) {
142
- if (withAreaCode) {
143
- const phone = value.split('-');
144
+ const valueNumber = typeof value === 'object' ? value.number : value;
145
+ if (prefix !== undefined && editablePrefix) {
146
+ this.setState({ phoneNumber: valueNumber });
147
+ if (typeof value !== 'object') onChange({ number: valueNumber, prefix });
148
+ } else if (withAreaCode) {
149
+ const phone = valueNumber.split('-');
144
150
  if (phone.length === 2) {
145
151
  const newAreaCodeState = this.getAreaCodeState(phone[0]);
146
152
  this.setState({
@@ -149,7 +155,7 @@ class UTPhoneInput extends PureComponent {
149
155
  });
150
156
  }
151
157
  } else {
152
- this.setState({ phoneNumber: value });
158
+ this.setState({ phoneNumber: valueNumber });
153
159
  }
154
160
  } else {
155
161
  this.setState({
@@ -164,7 +170,7 @@ class UTPhoneInput extends PureComponent {
164
170
  render() {
165
171
  const {
166
172
  areaCodePlaceholder,
167
- countryCode,
173
+ prefix,
168
174
  dataTestId,
169
175
  disabled,
170
176
  error,
@@ -235,9 +241,7 @@ class UTPhoneInput extends PureComponent {
235
241
  disabled={(withAreaCode && !isValidCode) || disabled || readOnly}
236
242
  error={hasError && !areaCodeError}
237
243
  leftAdornments={
238
- countryCode && !withAreaCode
239
- ? [{ name: COMPONENT_KEYS.PREFIX, props: { text: countryCode } }]
240
- : []
244
+ prefix && !withAreaCode ? [{ name: COMPONENT_KEYS.PREFIX, props: { text: prefix } }] : []
241
245
  }
242
246
  maxLength={withAreaCode ? maxLength - (areaCode?.length || 0) : maxLength}
243
247
  onBlur={this.handleSecondBlur}
@@ -288,7 +292,6 @@ UTPhoneInput.defaultProps = {
288
292
 
289
293
  UTPhoneInput.propTypes = {
290
294
  areaCodePlaceholder: string,
291
- countryCode: string,
292
295
  dataTestId: string,
293
296
  disabled: bool,
294
297
  error: string,
@@ -300,6 +303,8 @@ UTPhoneInput.propTypes = {
300
303
  onFocus: func,
301
304
  onSubmitEditing: func,
302
305
  placeholder: string,
306
+ prefix: string,
307
+ editablePrefix: bool,
303
308
  readOnly: bool,
304
309
  required: bool,
305
310
  RightIcon: elementType,
@@ -310,7 +315,7 @@ UTPhoneInput.propTypes = {
310
315
  invalidAreaCodeError: string
311
316
  }),
312
317
  validations: validationDataProptypes,
313
- value: string,
318
+ value: oneOfType([string, shape({ number: string, prefix: string })]),
314
319
  withAreaCode: bool
315
320
  };
316
321
 
package/lib/index.js CHANGED
@@ -44,6 +44,7 @@ export { default as UTButton } from './components/UTButton';
44
44
  export { default as UTButtonGroup } from './components/UTButtonGroup';
45
45
  export { default as UTCBUInput } from './components/UTCBUInput';
46
46
  export { default as UTCheckBox } from './components/UTCheckBox';
47
+ export { default as UTCoupon } from './components/UTCoupon';
47
48
  export { default as UTCuit } from './components/UTCuit';
48
49
  export { default as UTCheckList } from './components/UTCheckList';
49
50
  export { default as UTDataCategory } from './components/UTDataCategory';
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@widergy/mobile-ui",
3
3
  "description": "Widergy Mobile Components",
4
4
  "author": "widergy",
5
- "version": "2.9.7",
5
+ "version": "2.11.0",
6
6
  "repository": "https://github.com/widergy/mobile-ui.git",
7
7
  "main": "lib/index.js",
8
8
  "files": [