@@ -46,6 +46,9 @@ type Product = CompleteConfig['payments']['products'][keyof CompleteConfig['paym
4646type Price = ( Product [ 'prices' ] & object ) [ string ] ;
4747type PricesObject = Exclude < Product [ 'prices' ] , 'include-by-default' > ;
4848
49+ const DEFAULT_INTERVAL_UNITS : DayInterval [ 1 ] [ ] = [ 'day' , 'week' , 'month' , 'year' ] ;
50+ const PRICE_INTERVAL_UNITS : DayInterval [ 1 ] [ ] = [ 'week' , 'month' , 'year' ] ;
51+
4952
5053function intervalLabel ( tuple : DayInterval | undefined ) : string | null {
5154 if ( ! tuple ) return null ;
@@ -92,6 +95,7 @@ function IntervalPopover({
9295 setCount,
9396 onChange,
9497 noneLabel = 'one time' ,
98+ allowedUnits,
9599} : {
96100 readOnly ?: boolean ,
97101 intervalText : string | null ,
@@ -103,8 +107,25 @@ function IntervalPopover({
103107 setCount : ( n : number ) => void ,
104108 onChange : ( interval : DayInterval | null ) => void ,
105109 noneLabel ?: string ,
110+ allowedUnits ?: DayInterval [ 1 ] [ ] ,
106111} ) {
107112 const [ open , setOpen ] = useState ( false ) ;
113+ const buttonLabels : Record < DayInterval [ 1 ] , string > = {
114+ day : 'daily' ,
115+ week : 'weekly' ,
116+ month : 'monthly' ,
117+ year : 'yearly' ,
118+ } ;
119+
120+ const units = allowedUnits ?? DEFAULT_INTERVAL_UNITS ;
121+ const normalizedUnits = units . length > 0 ? units : DEFAULT_INTERVAL_UNITS ;
122+ const defaultUnit = ( normalizedUnits [ 0 ] ?? 'month' ) as DayInterval [ 1 ] ;
123+ const effectiveUnit = unit && normalizedUnits . includes ( unit ) ? unit : defaultUnit ;
124+ const isIntervalUnit = intervalSelection !== 'custom' && intervalSelection !== 'one-time' ;
125+ const effectiveSelection : 'one-time' | 'custom' | DayInterval [ 1 ] =
126+ isIntervalUnit && ! normalizedUnits . includes ( intervalSelection )
127+ ? 'custom'
128+ : intervalSelection ;
108129
109130 const selectOneTime = ( ) => {
110131 setIntervalSelection ( 'one-time' ) ;
@@ -114,19 +135,21 @@ function IntervalPopover({
114135 setOpen ( false ) ;
115136 } ;
116137
117- const selectFixed = ( unit : DayInterval [ 1 ] ) => {
118- setIntervalSelection ( unit ) ;
119- setUnit ( unit ) ;
138+ const selectFixed = ( unitOption : DayInterval [ 1 ] ) => {
139+ if ( ! normalizedUnits . includes ( unitOption ) ) return ;
140+ setIntervalSelection ( unitOption ) ;
141+ setUnit ( unitOption ) ;
120142 setCount ( 1 ) ;
121- if ( ! readOnly ) onChange ( [ 1 , unit ] ) ;
143+ if ( ! readOnly ) onChange ( [ 1 , unitOption ] ) ;
122144 setOpen ( false ) ;
123145 } ;
124146
125- const applyCustom = ( count : number , unit : DayInterval [ 1 ] ) => {
147+ const applyCustom = ( countValue : number , maybeUnit ?: DayInterval [ 1 ] ) => {
148+ const safeUnit = maybeUnit && normalizedUnits . includes ( maybeUnit ) ? maybeUnit : defaultUnit ;
126149 setIntervalSelection ( 'custom' ) ;
127- setUnit ( unit ) ;
128- setCount ( count ) ;
129- if ( ! readOnly ) onChange ( [ count , unit ] ) ;
150+ setUnit ( safeUnit ) ;
151+ setCount ( countValue ) ;
152+ if ( ! readOnly ) onChange ( [ countValue , safeUnit ] ) ;
130153 } ;
131154
132155 const triggerLabel = intervalText || noneLabel ;
@@ -142,60 +165,38 @@ function IntervalPopover({
142165 < PopoverContent align = "start" className = "w-60 p-2" >
143166 < div className = "flex flex-col gap-1" >
144167 < Button
145- variant = { intervalSelection === 'one-time' ? 'secondary' : 'ghost' }
168+ variant = { effectiveSelection === 'one-time' ? 'secondary' : 'ghost' }
146169 size = "sm"
147170 className = "justify-start"
148171 onClick = { selectOneTime }
149172 >
150173 { noneLabel }
151174 </ Button >
152- < Button
153- variant = { intervalSelection === 'day' ? 'secondary' : 'ghost' }
154- size = "sm"
155- className = "justify-start"
156- onClick = { ( ) => selectFixed ( 'day' ) }
157- >
158- daily
159- </ Button >
160- < Button
161- variant = { intervalSelection === 'week' ? 'secondary' : 'ghost' }
162- size = "sm"
163- className = "justify-start"
164- onClick = { ( ) => selectFixed ( 'week' ) }
165- >
166- weekly
167- </ Button >
168- < Button
169- variant = { intervalSelection === 'month' ? 'secondary' : 'ghost' }
170- size = "sm"
171- className = "justify-start"
172- onClick = { ( ) => selectFixed ( 'month' ) }
173- >
174- monthly
175- </ Button >
176- < Button
177- variant = { intervalSelection === 'year' ? 'secondary' : 'ghost' }
178- size = "sm"
179- className = "justify-start"
180- onClick = { ( ) => selectFixed ( 'year' ) }
181- >
182- yearly
183- </ Button >
175+ { normalizedUnits . map ( ( unitOption ) => (
176+ < Button
177+ key = { unitOption }
178+ variant = { effectiveSelection === unitOption ? 'secondary' : 'ghost' }
179+ size = "sm"
180+ className = "justify-start"
181+ onClick = { ( ) => selectFixed ( unitOption ) }
182+ >
183+ { buttonLabels [ unitOption ] }
184+ </ Button >
185+ ) ) }
184186
185187 < Button
186- variant = { intervalSelection === 'custom' ? 'secondary' : 'ghost' }
188+ variant = { effectiveSelection === 'custom' ? 'secondary' : 'ghost' }
187189 size = "sm"
188190 className = "justify-start"
189191 onClick = { ( ) => {
190192 setIntervalSelection ( 'custom' ) ;
191- const nextUnit = ( unit || 'month' ) as DayInterval [ 1 ] ;
192- setUnit ( nextUnit ) ;
193+ setUnit ( effectiveUnit ) ;
193194 } }
194195 >
195196 custom
196197 </ Button >
197198
198- { intervalSelection === 'custom' && (
199+ { effectiveSelection === 'custom' && (
199200 < div className = "mt-2 px-1" >
200201 < div className = "text-xs text-muted-foreground mb-1" > Custom</ div >
201202 < div className = "flex items-center gap-2" >
@@ -209,13 +210,13 @@ function IntervalPopover({
209210 const v = e . target . value ;
210211 if ( ! / ^ \d * $ / . test ( v ) ) return ;
211212 const n = v === '' ? 0 : parseInt ( v , 10 ) ;
212- applyCustom ( n , ( unit || 'month' ) as DayInterval [ 1 ] ) ;
213+ applyCustom ( n , effectiveUnit ) ;
213214 } }
214215 />
215216 </ div >
216217 < div className = "w-24" >
217218 < Select
218- value = { ( unit || 'month' ) as DayInterval [ 1 ] }
219+ value = { effectiveUnit }
219220 onValueChange = { ( u ) => {
220221 const newUnit = u as DayInterval [ 1 ] ;
221222 applyCustom ( count , newUnit ) ;
@@ -225,10 +226,11 @@ function IntervalPopover({
225226 < SelectValue />
226227 </ SelectTrigger >
227228 < SelectContent >
228- < SelectItem value = "day" > day</ SelectItem >
229- < SelectItem value = "week" > week</ SelectItem >
230- < SelectItem value = "month" > month</ SelectItem >
231- < SelectItem value = "year" > year</ SelectItem >
229+ { normalizedUnits . map ( ( unitOption ) => (
230+ < SelectItem key = { unitOption } value = { unitOption } >
231+ { unitOption }
232+ </ SelectItem >
233+ ) ) }
232234 </ SelectContent >
233235 </ Select >
234236 </ div >
@@ -248,6 +250,7 @@ type ProductEditableInputProps = {
248250 readOnly ?: boolean ,
249251 placeholder ?: string ,
250252 inputClassName ?: string ,
253+ transform ?: ( value : string ) => string ,
251254} ;
252255
253256function ProductEditableInput ( {
@@ -256,6 +259,7 @@ function ProductEditableInput({
256259 readOnly,
257260 placeholder,
258261 inputClassName,
262+ transform,
259263} : ProductEditableInputProps ) {
260264 const [ isActive , setIsActive ] = useState ( false ) ;
261265
@@ -278,7 +282,8 @@ function ProductEditableInput({
278282 < Input
279283 value = { value }
280284 onChange = { ( event ) => {
281- const nextValue = event . target . value ;
285+ const rawValue = event . target . value ;
286+ const nextValue = transform ? transform ( rawValue ) : rawValue ;
282287 void onUpdate ?.( nextValue ) ;
283288 } }
284289 placeholder = { placeholder }
@@ -383,6 +388,7 @@ function ProductPriceRow({
383388 setIntervalSelection = { setIntervalSelection }
384389 setUnit = { setPriceInterval }
385390 setCount = { setIntervalCount }
391+ allowedUnits = { PRICE_INTERVAL_UNITS }
386392 onChange = { ( interval ) => {
387393 if ( readOnly ) return ;
388394 const normalized = amount === '' ? '0.00' : ( Number . isNaN ( parseFloat ( amount ) ) ? '0.00' : parseFloat ( amount ) . toFixed ( 2 ) ) ;
@@ -496,7 +502,7 @@ function ProductItemRow({
496502 < Popover open = { itemSelectOpen } onOpenChange = { setItemSelectOpen } >
497503 < PopoverTrigger >
498504 < div className = "text-sm px-2 py-0.5 rounded bg-muted hover:bg-muted/70 cursor-pointer select-none flex items-center gap-1" >
499- { itemDisplayName }
505+ { itemId }
500506 < ChevronsUpDown className = "h-4 w-4" />
501507 </ div >
502508 </ PopoverTrigger >
@@ -627,7 +633,7 @@ function ProductItemRow({
627633 < ChevronDown className = { cn ( "h-4 w-4 transition-transform" , isOpen ? "rotate-0" : "-rotate-90" ) } />
628634 </ button >
629635 </ CollapsibleTrigger >
630- < div className = "text-sm" > { itemDisplayName } </ div >
636+ < div className = "text-sm" > { itemId } </ div >
631637 < div className = "ml-auto w-16 text-right text-sm text-muted-foreground tabular-nums" > { prettyPrintWithMagnitudes ( item . quantity ) } </ div >
632638 < div className = "ml-2" >
633639 < div className = "text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground" > { shortRepeatText } </ div >
@@ -863,6 +869,7 @@ function ProductCard({ id, activeType, product, allProducts, existingItems, onSa
863869 readOnly = { ! isDraft || ! isEditing }
864870 placeholder = { "Product ID" }
865871 inputClassName = "text-xs font-mono text-center text-muted-foreground"
872+ transform = { ( value ) => value . toLowerCase ( ) }
866873 />
867874 < ProductEditableInput
868875 value = { draft . displayName || "" }
0 commit comments