@lifeaz/lors-helper 1.0.27 → 1.1.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/README.md +1 -1
- package/dist/main.d.ts +192 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +430 -0
- package/package.json +3 -3
- package/src/main.ts +681 -0
package/dist/main.js
CHANGED
|
@@ -262,6 +262,7 @@ export const CartSchema = z.object({
|
|
|
262
262
|
status: CartStatusSchema,
|
|
263
263
|
currentStep: CartStepSchema,
|
|
264
264
|
maxReachedStep: CartStepSchema,
|
|
265
|
+
stepOrder: z.array(CartStepSchema).nullable().default(null),
|
|
265
266
|
profile: ProfileTypeSchema.nullable(),
|
|
266
267
|
offerType: OfferTypeSchema.nullable(),
|
|
267
268
|
paymentMethod: PaymentMethodSchema.nullable(),
|
|
@@ -468,3 +469,432 @@ export const PartnerSchema = z.object({
|
|
|
468
469
|
blockedDeliveryMethods: z.array(DeliveryMethodSchema),
|
|
469
470
|
notices: z.array(PartnerNoticeSchema),
|
|
470
471
|
});
|
|
472
|
+
export const MvtStatusSchema = z.union([z.literal('_Draft_'), z.literal('_Scheduled_'), z.literal('_Running_'), z.literal('_Paused_'), z.literal('_Stopped_'), z.literal('_Archived_')]);
|
|
473
|
+
export const MvtScopeSchema = z.union([z.literal('_Storefront_'), z.literal('_Checkout_'), z.literal('_Global_')]);
|
|
474
|
+
export const MvtUnitSchema = z.union([z.literal('_Visitor_'), z.literal('_Session_'), z.literal('_Cart_')]);
|
|
475
|
+
export const MvtDeviceSchema = z.union([z.literal('_Mobile_'), z.literal('_Tablet_'), z.literal('_Desktop_')]);
|
|
476
|
+
export const MvtNodeKindSchema = z.union([z.literal('_And_'), z.literal('_Or_'), z.literal('_Not_'), z.literal('_Leaf_')]);
|
|
477
|
+
export const MvtOperatorSchema = z.union([
|
|
478
|
+
z.literal('_Device_In_'),
|
|
479
|
+
z.literal('_Lang_In_'),
|
|
480
|
+
z.literal('_Country_In_'),
|
|
481
|
+
z.literal('_Profile_In_'),
|
|
482
|
+
z.literal('_Partner_In_'),
|
|
483
|
+
z.literal('_Partner_Absent_'),
|
|
484
|
+
z.literal('_Campaign_Seen_'),
|
|
485
|
+
z.literal('_Campaign_Not_Seen_'),
|
|
486
|
+
z.literal('_Visitor_Is_New_'),
|
|
487
|
+
z.literal('_Visitor_Is_Returning_'),
|
|
488
|
+
z.literal('_Utm_Source_In_'),
|
|
489
|
+
z.literal('_Utm_Medium_In_'),
|
|
490
|
+
z.literal('_Utm_Campaign_In_'),
|
|
491
|
+
z.literal('_Referrer_Matches_'),
|
|
492
|
+
z.literal('_Path_Matches_'),
|
|
493
|
+
z.literal('_Has_Cart_'),
|
|
494
|
+
z.literal('_Hour_Of_Day_Between_'),
|
|
495
|
+
z.literal('_Day_Of_Week_In_'),
|
|
496
|
+
]);
|
|
497
|
+
export const MVT_NULLARY_OPERATORS = [
|
|
498
|
+
'_Partner_Absent_',
|
|
499
|
+
'_Visitor_Is_New_',
|
|
500
|
+
'_Visitor_Is_Returning_',
|
|
501
|
+
'_Has_Cart_',
|
|
502
|
+
];
|
|
503
|
+
export const MvtOverrideOpSchema = z.union([z.literal('set'), z.literal('merge'), z.literal('remove'), z.literal('reorder'), z.literal('insert'), z.literal('patch')]);
|
|
504
|
+
export const MvtOverrideSchema = z.object({
|
|
505
|
+
slot: z.string().min(1),
|
|
506
|
+
op: MvtOverrideOpSchema,
|
|
507
|
+
value: z.unknown().optional(),
|
|
508
|
+
at: z.number().int().nonnegative().optional(),
|
|
509
|
+
match: z.object({ by: z.string().min(1), equals: z.string() }).optional(),
|
|
510
|
+
});
|
|
511
|
+
export const MvtAudienceNodeSchema = z.lazy(() => z.object({
|
|
512
|
+
kind: MvtNodeKindSchema,
|
|
513
|
+
operator: MvtOperatorSchema.nullable().optional(),
|
|
514
|
+
values: z.array(z.string()).optional(),
|
|
515
|
+
children: z.array(z.lazy(() => MvtAudienceNodeSchema)).optional(),
|
|
516
|
+
}));
|
|
517
|
+
export const MvtVariantSchema = z.object({
|
|
518
|
+
id: uuidSchema,
|
|
519
|
+
key: z.string().min(1),
|
|
520
|
+
name: z.string(),
|
|
521
|
+
weight: z.number().int().nonnegative(),
|
|
522
|
+
control: z.boolean(),
|
|
523
|
+
overrides: z.array(MvtOverrideSchema),
|
|
524
|
+
});
|
|
525
|
+
export const MvtExperimentSchema = z.object({
|
|
526
|
+
id: uuidSchema,
|
|
527
|
+
key: z.string().min(1),
|
|
528
|
+
name: z.string(),
|
|
529
|
+
description: z.string().nullable(),
|
|
530
|
+
status: MvtStatusSchema,
|
|
531
|
+
scope: MvtScopeSchema,
|
|
532
|
+
unit: MvtUnitSchema,
|
|
533
|
+
salt: z.string().min(1),
|
|
534
|
+
startsAt: z.string().nullable(),
|
|
535
|
+
endsAt: z.string().nullable(),
|
|
536
|
+
priority: z.number().int(),
|
|
537
|
+
exclusionGroup: z.string().nullable(),
|
|
538
|
+
trafficPct: z.number().int().min(0).max(100),
|
|
539
|
+
sticky: z.boolean(),
|
|
540
|
+
audience: MvtAudienceNodeSchema.nullable(),
|
|
541
|
+
variants: z.array(MvtVariantSchema),
|
|
542
|
+
});
|
|
543
|
+
export const MvtConfigSchema = z.object({
|
|
544
|
+
version: z.string(),
|
|
545
|
+
enabled: z.boolean(),
|
|
546
|
+
experiments: z.array(MvtExperimentSchema),
|
|
547
|
+
});
|
|
548
|
+
const MVT_RETURNING_AFTER_MS = 30 * 60 * 1000;
|
|
549
|
+
export function mvtHash(input) {
|
|
550
|
+
let hash = 0x811c9dc5;
|
|
551
|
+
for (let i = 0; i < input.length; i++) {
|
|
552
|
+
hash ^= input.charCodeAt(i);
|
|
553
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
554
|
+
}
|
|
555
|
+
return hash >>> 0;
|
|
556
|
+
}
|
|
557
|
+
export function mvtBucket(salt, unitId, namespace = 'variant') {
|
|
558
|
+
return mvtHash(`${salt}:${namespace}:${unitId}`) % 10000;
|
|
559
|
+
}
|
|
560
|
+
export function mvtIsEligibleForTraffic(experiment, unitId) {
|
|
561
|
+
if (experiment.trafficPct >= 100)
|
|
562
|
+
return true;
|
|
563
|
+
if (experiment.trafficPct <= 0)
|
|
564
|
+
return false;
|
|
565
|
+
return mvtBucket(experiment.salt, unitId, 'traffic') < experiment.trafficPct * 100;
|
|
566
|
+
}
|
|
567
|
+
export function mvtPickVariant(experiment, unitId) {
|
|
568
|
+
const variants = [...experiment.variants].sort((a, b) => a.key.localeCompare(b.key));
|
|
569
|
+
const total = variants.reduce((sum, v) => sum + Math.max(v.weight, 0), 0);
|
|
570
|
+
if (!variants.length || total <= 0)
|
|
571
|
+
return null;
|
|
572
|
+
const target = (mvtBucket(experiment.salt, unitId) / 10000) * total;
|
|
573
|
+
let cursor = 0;
|
|
574
|
+
for (const variant of variants) {
|
|
575
|
+
cursor += Math.max(variant.weight, 0);
|
|
576
|
+
if (target < cursor)
|
|
577
|
+
return variant;
|
|
578
|
+
}
|
|
579
|
+
return variants[variants.length - 1];
|
|
580
|
+
}
|
|
581
|
+
function mvtMatchesPattern(value, patterns) {
|
|
582
|
+
if (value === null)
|
|
583
|
+
return false;
|
|
584
|
+
const haystack = value.toLowerCase();
|
|
585
|
+
return patterns.some(pattern => {
|
|
586
|
+
const needle = pattern.trim().toLowerCase();
|
|
587
|
+
if (!needle)
|
|
588
|
+
return false;
|
|
589
|
+
if (needle.startsWith('*') && needle.endsWith('*'))
|
|
590
|
+
return haystack.includes(needle.slice(1, -1));
|
|
591
|
+
if (needle.startsWith('*'))
|
|
592
|
+
return haystack.endsWith(needle.slice(1));
|
|
593
|
+
if (needle.endsWith('*'))
|
|
594
|
+
return haystack.startsWith(needle.slice(0, -1));
|
|
595
|
+
return haystack === needle;
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
function mvtEvaluateLeaf(operator, values, context) {
|
|
599
|
+
const lower = values.map(v => v.toLowerCase());
|
|
600
|
+
switch (operator) {
|
|
601
|
+
case '_Device_In_': return values.includes(context.device);
|
|
602
|
+
case '_Lang_In_': return lower.includes(context.lang.toLowerCase());
|
|
603
|
+
case '_Country_In_': return context.country !== null && lower.includes(context.country.toLowerCase());
|
|
604
|
+
case '_Profile_In_': return context.profile !== null && values.includes(context.profile);
|
|
605
|
+
case '_Partner_In_': return context.partner !== null && lower.includes(context.partner.toLowerCase());
|
|
606
|
+
case '_Partner_Absent_': return context.partner === null;
|
|
607
|
+
case '_Campaign_Seen_': return values.some(v => context.seen[`campaign:${v}`] !== undefined);
|
|
608
|
+
case '_Campaign_Not_Seen_': return values.every(v => context.seen[`campaign:${v}`] === undefined);
|
|
609
|
+
case '_Visitor_Is_New_': return context.now - context.firstSeenAt < MVT_RETURNING_AFTER_MS;
|
|
610
|
+
case '_Visitor_Is_Returning_': return context.now - context.firstSeenAt >= MVT_RETURNING_AFTER_MS;
|
|
611
|
+
case '_Utm_Source_In_': return mvtMatchesPattern(context.utm.source, values);
|
|
612
|
+
case '_Utm_Medium_In_': return mvtMatchesPattern(context.utm.medium, values);
|
|
613
|
+
case '_Utm_Campaign_In_': return mvtMatchesPattern(context.utm.campaign, values);
|
|
614
|
+
case '_Referrer_Matches_': return mvtMatchesPattern(context.referrer, values);
|
|
615
|
+
case '_Path_Matches_': return mvtMatchesPattern(context.path, values);
|
|
616
|
+
case '_Has_Cart_': return context.hasCart;
|
|
617
|
+
case '_Hour_Of_Day_Between_': {
|
|
618
|
+
const hour = new Date(context.now).getHours();
|
|
619
|
+
const from = Number(values[0] ?? 0);
|
|
620
|
+
const to = Number(values[1] ?? 23);
|
|
621
|
+
return from <= to ? hour >= from && hour <= to : hour >= from || hour <= to;
|
|
622
|
+
}
|
|
623
|
+
case '_Day_Of_Week_In_': return values.map(Number).includes(new Date(context.now).getDay());
|
|
624
|
+
default: return false;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
export function mvtEvaluateAudience(node, context) {
|
|
628
|
+
if (!node)
|
|
629
|
+
return true;
|
|
630
|
+
switch (node.kind) {
|
|
631
|
+
case '_And_': return (node.children ?? []).every(child => mvtEvaluateAudience(child, context));
|
|
632
|
+
case '_Or_': return (node.children ?? []).some(child => mvtEvaluateAudience(child, context));
|
|
633
|
+
case '_Not_': return !(node.children ?? []).some(child => mvtEvaluateAudience(child, context));
|
|
634
|
+
case '_Leaf_': return node.operator ? mvtEvaluateLeaf(node.operator, node.values ?? [], context) : false;
|
|
635
|
+
default: return false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
export function mvtIsWithinWindow(experiment, now) {
|
|
639
|
+
if (experiment.startsAt && Date.parse(experiment.startsAt) > now)
|
|
640
|
+
return false;
|
|
641
|
+
if (experiment.endsAt && Date.parse(experiment.endsAt) <= now)
|
|
642
|
+
return false;
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
export function mvtIsLive(experiment, now) {
|
|
646
|
+
return experiment.status === '_Running_' && mvtIsWithinWindow(experiment, now);
|
|
647
|
+
}
|
|
648
|
+
export function mvtResolve(config, context, forced = {}) {
|
|
649
|
+
if (!config.enabled)
|
|
650
|
+
return [];
|
|
651
|
+
const decisions = [];
|
|
652
|
+
const claimed = new Set();
|
|
653
|
+
const experiments = [...config.experiments].sort((a, b) => b.priority - a.priority || a.key.localeCompare(b.key));
|
|
654
|
+
for (const experiment of experiments) {
|
|
655
|
+
const forcedKey = forced[experiment.key];
|
|
656
|
+
if (forcedKey) {
|
|
657
|
+
const variant = experiment.variants.find(v => v.key === forcedKey);
|
|
658
|
+
if (variant)
|
|
659
|
+
decisions.push(mvtToDecision(experiment, variant));
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (!mvtIsLive(experiment, context.now))
|
|
663
|
+
continue;
|
|
664
|
+
if (experiment.exclusionGroup && claimed.has(experiment.exclusionGroup))
|
|
665
|
+
continue;
|
|
666
|
+
if (!mvtEvaluateAudience(experiment.audience, context))
|
|
667
|
+
continue;
|
|
668
|
+
if (!mvtIsEligibleForTraffic(experiment, context.unitId))
|
|
669
|
+
continue;
|
|
670
|
+
const variant = mvtPickVariant(experiment, context.unitId);
|
|
671
|
+
if (!variant)
|
|
672
|
+
continue;
|
|
673
|
+
if (experiment.exclusionGroup)
|
|
674
|
+
claimed.add(experiment.exclusionGroup);
|
|
675
|
+
decisions.push(mvtToDecision(experiment, variant));
|
|
676
|
+
}
|
|
677
|
+
return decisions;
|
|
678
|
+
}
|
|
679
|
+
function mvtToDecision(experiment, variant) {
|
|
680
|
+
return {
|
|
681
|
+
experimentKey: experiment.key,
|
|
682
|
+
experimentId: experiment.id,
|
|
683
|
+
variantKey: variant.key,
|
|
684
|
+
variantId: variant.id,
|
|
685
|
+
control: variant.control,
|
|
686
|
+
overrides: variant.overrides,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
const i18nTextSchema = z.record(LangSchema, z.string());
|
|
690
|
+
const colorSchema = z.string().regex(/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i);
|
|
691
|
+
function slot(definition) {
|
|
692
|
+
return definition;
|
|
693
|
+
}
|
|
694
|
+
export const MVT_SLOTS = [
|
|
695
|
+
slot({ key: 'storefront.sections.order', scope: '_Storefront_', area: 'Structure', label: 'Section order', hint: 'Reorder the blocks of a page. Blocks not listed keep their place at the end.', kind: 'list', ops: ['reorder', 'set'], itemKey: 'id', schema: z.array(z.string()) }),
|
|
696
|
+
slot({ key: 'storefront.sections.hidden', scope: '_Storefront_', area: 'Structure', label: 'Hidden sections', hint: 'Blocks to take off the page entirely.', kind: 'list', ops: ['set'], schema: z.array(z.string()) }),
|
|
697
|
+
slot({ key: 'storefront.texts', scope: '_Storefront_', area: 'Structure', label: 'Text overrides', hint: 'Replace any piece of copy, written per language.', kind: 'record', entryKind: 'i18n', ops: ['merge', 'set'], schema: z.record(z.string(), i18nTextSchema) }),
|
|
698
|
+
slot({ key: 'storefront.links', scope: '_Storefront_', area: 'Structure', label: 'Link targets', hint: 'Repoint a link somewhere else, for instance from the product sheet to a comparison page.', kind: 'record', entryKind: 'text', ops: ['merge', 'set'], schema: z.record(z.string(), z.string()) }),
|
|
699
|
+
slot({
|
|
700
|
+
key: 'storefront.ctas', scope: '_Storefront_', area: 'CTA', label: 'Call to action list',
|
|
701
|
+
hint: 'Hide, reorder or reword the buttons of a page.',
|
|
702
|
+
kind: 'list', ops: ['remove', 'reorder', 'patch', 'insert', 'set'], itemKey: 'label',
|
|
703
|
+
fields: [
|
|
704
|
+
{ key: 'label', label: 'Label', kind: 'text' },
|
|
705
|
+
{ key: 'variant', label: 'Style', kind: 'enum', options: ['primary', 'secondary', 'tertiary'] },
|
|
706
|
+
{ key: 'route', label: 'Internal route', kind: 'text' },
|
|
707
|
+
{ key: 'href', label: 'External link', kind: 'text' },
|
|
708
|
+
],
|
|
709
|
+
schema: z.array(z.record(z.string(), z.unknown())),
|
|
710
|
+
}),
|
|
711
|
+
slot({
|
|
712
|
+
key: 'storefront.cta.colors', scope: '_Storefront_', area: 'CTA', label: 'Call to action colors',
|
|
713
|
+
hint: 'Recolour the buttons, per style.',
|
|
714
|
+
kind: 'object', ops: ['merge', 'set'],
|
|
715
|
+
fields: [
|
|
716
|
+
{ key: 'primary', label: 'Primary', kind: 'color' },
|
|
717
|
+
{ key: 'secondary', label: 'Secondary', kind: 'color' },
|
|
718
|
+
{ key: 'tertiary', label: 'Tertiary', kind: 'color' },
|
|
719
|
+
],
|
|
720
|
+
schema: z.record(z.string(), colorSchema),
|
|
721
|
+
}),
|
|
722
|
+
slot({ key: 'storefront.header.mobile.ctas.hidden', scope: '_Storefront_', area: 'CTA', label: 'Burger menu hidden CTAs', hint: 'Buttons to drop from the mobile menu only.', kind: 'list', ops: ['set'], schema: z.array(z.string()) }),
|
|
723
|
+
slot({
|
|
724
|
+
key: 'campaign.banner.colors', scope: '_Global_', area: 'Flash offer', label: 'Banner colors',
|
|
725
|
+
hint: 'Recolour the flash offer banner.',
|
|
726
|
+
kind: 'object', ops: ['merge', 'set'],
|
|
727
|
+
fields: [
|
|
728
|
+
{ key: 'background', label: 'Background', kind: 'color' },
|
|
729
|
+
{ key: 'text', label: 'Text', kind: 'color' },
|
|
730
|
+
{ key: 'tagBackground', label: 'Tag background', kind: 'color' },
|
|
731
|
+
{ key: 'tagText', label: 'Tag text', kind: 'color' },
|
|
732
|
+
{ key: 'countdownText', label: 'Countdown text', kind: 'color' },
|
|
733
|
+
],
|
|
734
|
+
schema: z.record(z.string(), colorSchema),
|
|
735
|
+
}),
|
|
736
|
+
slot({ key: 'campaign.banner.text', scope: '_Global_', area: 'Flash offer', label: 'Banner text', hint: 'The wording of the flash offer banner.', kind: 'i18n', ops: ['set'], schema: i18nTextSchema }),
|
|
737
|
+
slot({ key: 'campaign.banner.tag', scope: '_Global_', area: 'Flash offer', label: 'Banner tag', hint: 'The small badge at the start of the banner.', kind: 'i18n', ops: ['set'], schema: i18nTextSchema }),
|
|
738
|
+
slot({ key: 'campaign.banner.showCountdown', scope: '_Global_', area: 'Flash offer', label: 'Show countdown', hint: 'Whether the banner counts down to the end of the offer.', kind: 'boolean', ops: ['set'], schema: z.boolean() }),
|
|
739
|
+
slot({ key: 'campaign.banner.placement', scope: '_Global_', area: 'Flash offer', label: 'Banner placement', hint: 'Where the banner sits on the page.', kind: 'enum', ops: ['set'], options: ['top', 'below-header', 'sticky-bottom'], schema: z.enum(['top', 'below-header', 'sticky-bottom']) }),
|
|
740
|
+
slot({ key: 'campaign.priceTag.text', scope: '_Global_', area: 'Flash offer', label: 'Discount badge text', hint: 'The wording of the discount badge shown next to prices.', kind: 'i18n', ops: ['set'], schema: i18nTextSchema }),
|
|
741
|
+
slot({ key: 'campaign.priceTag.placement', scope: '_Global_', area: 'Flash offer', label: 'Discount badge placement', hint: 'Where the discount badge sits relative to the price.', kind: 'enum', ops: ['set'], options: ['price-inline', 'card-corner', 'above-price', 'hidden'], schema: z.enum(['price-inline', 'card-corner', 'above-price', 'hidden']) }),
|
|
742
|
+
slot({
|
|
743
|
+
key: 'campaign.priceTag.colors', scope: '_Global_', area: 'Flash offer', label: 'Discount badge colors',
|
|
744
|
+
hint: 'Recolour the discount badge.',
|
|
745
|
+
kind: 'object', ops: ['merge', 'set'],
|
|
746
|
+
fields: [
|
|
747
|
+
{ key: 'background', label: 'Background', kind: 'color' },
|
|
748
|
+
{ key: 'text', label: 'Text', kind: 'color' },
|
|
749
|
+
],
|
|
750
|
+
schema: z.record(z.string(), colorSchema),
|
|
751
|
+
}),
|
|
752
|
+
slot({ key: 'campaign.ribbon.text', scope: '_Global_', area: 'Flash offer', label: 'Ribbon text', hint: 'The ribbon shown on the offer summary.', kind: 'i18n', ops: ['set'], schema: i18nTextSchema }),
|
|
753
|
+
slot({ key: 'popup.welcome.enabled', scope: '_Storefront_', area: 'Popup', label: 'Welcome popup enabled', hint: 'Whether the welcome popup appears at all.', kind: 'boolean', ops: ['set'], schema: z.boolean() }),
|
|
754
|
+
slot({ key: 'popup.welcome.delayMs', scope: '_Storefront_', area: 'Popup', label: 'Welcome popup delay', hint: 'How long to wait before showing it.', kind: 'duration', ops: ['set'], schema: z.number().int().min(0).max(600000) }),
|
|
755
|
+
slot({ key: 'popup.welcome.trigger', scope: '_Storefront_', area: 'Popup', label: 'Welcome popup trigger', hint: 'What makes it appear: a timer, scrolling, or the cursor leaving the page.', kind: 'enum', ops: ['set'], options: ['timer', 'scroll', 'exit-intent'], schema: z.enum(['timer', 'scroll', 'exit-intent']) }),
|
|
756
|
+
slot({
|
|
757
|
+
key: 'popup.welcome.frequency', scope: '_Storefront_', area: 'Popup', label: 'Welcome popup frequency',
|
|
758
|
+
hint: 'How often one visitor sees it again.',
|
|
759
|
+
kind: 'object', ops: ['merge', 'set'],
|
|
760
|
+
fields: [
|
|
761
|
+
{ key: 'mode', label: 'Mode', kind: 'enum', options: ['once', 'session', 'days'] },
|
|
762
|
+
{ key: 'days', label: 'Every N days', kind: 'number' },
|
|
763
|
+
],
|
|
764
|
+
schema: z.object({ mode: z.enum(['once', 'session', 'days']), days: z.number().int().min(1).max(365).optional() }),
|
|
765
|
+
}),
|
|
766
|
+
slot({
|
|
767
|
+
key: 'popup.welcome.content', scope: '_Storefront_', area: 'Popup', label: 'Welcome popup content',
|
|
768
|
+
hint: 'The wording and image of the welcome popup.',
|
|
769
|
+
kind: 'object', ops: ['merge', 'set'],
|
|
770
|
+
fields: [
|
|
771
|
+
{ key: 'title', label: 'Title', kind: 'i18n' },
|
|
772
|
+
{ key: 'subtitle', label: 'Subtitle', kind: 'i18n' },
|
|
773
|
+
{ key: 'ctaLabel', label: 'Button label', kind: 'i18n' },
|
|
774
|
+
{ key: 'confirmationTitle', label: 'Confirmation title', kind: 'i18n' },
|
|
775
|
+
{ key: 'confirmationText', label: 'Confirmation text', kind: 'i18n' },
|
|
776
|
+
{ key: 'image', label: 'Image', kind: 'image' },
|
|
777
|
+
{ key: 'imageMobile', label: 'Image (mobile)', kind: 'image' },
|
|
778
|
+
],
|
|
779
|
+
schema: z.record(z.string(), z.unknown()),
|
|
780
|
+
}),
|
|
781
|
+
slot({ key: 'checkout.steps.order', scope: '_Checkout_', area: 'Funnel', label: 'Step order', hint: 'Reorder the funnel. Must remain a permutation of the six steps, or it is ignored.', kind: 'list', ops: ['reorder', 'set'], options: ['_Profile_', '_Offers_', '_Information_', '_Options_', '_Delivery_', '_Payment_'], schema: z.array(CartStepSchema) }),
|
|
782
|
+
slot({ key: 'checkout.steps.hidden', scope: '_Checkout_', area: 'Funnel', label: 'Hidden steps', hint: 'Steps to skip entirely.', kind: 'list', ops: ['set'], options: ['_Profile_', '_Offers_', '_Information_', '_Options_', '_Delivery_', '_Payment_'], schema: z.array(CartStepSchema) }),
|
|
783
|
+
slot({ key: 'checkout.options.preselected', scope: '_Checkout_', area: 'Funnel', label: 'Preselected options', hint: 'Options ticked by default on the options step.', kind: 'list', ops: ['set'], schema: z.array(z.string()) }),
|
|
784
|
+
slot({ key: 'checkout.progress.visible', scope: '_Checkout_', area: 'Funnel', label: 'Progress bar visible', hint: 'Whether a progress indicator is shown.', kind: 'boolean', ops: ['set'], schema: z.boolean() }),
|
|
785
|
+
slot({ key: 'checkout.progress.style', scope: '_Checkout_', area: 'Funnel', label: 'Progress bar style', hint: 'The shape the progress indicator takes.', kind: 'enum', ops: ['set'], options: ['bar', 'steps', 'percentage'], schema: z.enum(['bar', 'steps', 'percentage']) }),
|
|
786
|
+
slot({ key: 'checkout.chat.enabled', scope: '_Checkout_', area: 'Contact', label: 'Chat enabled', hint: 'Whether the chat widget loads inside the checkout.', kind: 'boolean', ops: ['set'], schema: z.boolean() }),
|
|
787
|
+
slot({ key: 'checkout.chat.delayMs', scope: '_Checkout_', area: 'Contact', label: 'Chat load delay', hint: 'How long to wait before loading the chat.', kind: 'duration', ops: ['set'], schema: z.number().int().min(0).max(600000) }),
|
|
788
|
+
slot({ key: 'checkout.exitIntent.enabled', scope: '_Checkout_', area: 'Contact', label: 'Exit popup enabled', hint: 'Whether a popup appears when the cursor leaves the page.', kind: 'boolean', ops: ['set'], schema: z.boolean() }),
|
|
789
|
+
slot({
|
|
790
|
+
key: 'checkout.exitIntent.content', scope: '_Checkout_', area: 'Contact', label: 'Exit popup content',
|
|
791
|
+
hint: 'The wording of the exit popup.',
|
|
792
|
+
kind: 'object', ops: ['merge', 'set'],
|
|
793
|
+
fields: [
|
|
794
|
+
{ key: 'title', label: 'Title', kind: 'i18n' },
|
|
795
|
+
{ key: 'text', label: 'Text', kind: 'i18n' },
|
|
796
|
+
{ key: 'ctaLabel', label: 'Button label', kind: 'i18n' },
|
|
797
|
+
],
|
|
798
|
+
schema: z.record(z.string(), i18nTextSchema),
|
|
799
|
+
}),
|
|
800
|
+
slot({ key: 'checkout.texts', scope: '_Checkout_', area: 'Structure', label: 'Text overrides', hint: 'Replace any piece of checkout copy, written per language.', kind: 'record', entryKind: 'i18n', ops: ['merge', 'set'], schema: z.record(z.string(), i18nTextSchema) }),
|
|
801
|
+
slot({
|
|
802
|
+
key: 'checkout.media.visible', scope: '_Checkout_', area: 'Structure', label: 'Media visibility',
|
|
803
|
+
hint: 'Show or hide the product photos, per device. Hiding them on mobile buys back a lot of height.',
|
|
804
|
+
kind: 'object', ops: ['merge', 'set'],
|
|
805
|
+
fields: [
|
|
806
|
+
{ key: 'mobile', label: 'Mobile', kind: 'boolean' },
|
|
807
|
+
{ key: 'tablet', label: 'Tablet', kind: 'boolean' },
|
|
808
|
+
{ key: 'desktop', label: 'Desktop', kind: 'boolean' },
|
|
809
|
+
],
|
|
810
|
+
schema: z.object({ mobile: z.boolean().optional(), tablet: z.boolean().optional(), desktop: z.boolean().optional() }),
|
|
811
|
+
}),
|
|
812
|
+
];
|
|
813
|
+
const MVT_SLOTS_BY_KEY = new Map(MVT_SLOTS.map(s => [s.key, s]));
|
|
814
|
+
export function mvtSlot(key) {
|
|
815
|
+
return MVT_SLOTS_BY_KEY.get(key) ?? null;
|
|
816
|
+
}
|
|
817
|
+
export function mvtSlotsForScope(scope) {
|
|
818
|
+
return MVT_SLOTS.filter(s => s.scope === scope || s.scope === '_Global_' || scope === '_Global_');
|
|
819
|
+
}
|
|
820
|
+
export function mvtValidateOverrides(overrides) {
|
|
821
|
+
const issues = [];
|
|
822
|
+
overrides.forEach((override, index) => {
|
|
823
|
+
const definition = mvtSlot(override.slot);
|
|
824
|
+
if (!definition) {
|
|
825
|
+
issues.push({ index, slot: override.slot, reason: 'unknown_slot' });
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (!definition.ops.includes(override.op)) {
|
|
829
|
+
issues.push({ index, slot: override.slot, reason: 'unsupported_op' });
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (override.op === 'remove' || override.op === 'reorder')
|
|
833
|
+
return;
|
|
834
|
+
if (override.op === 'merge' || override.op === 'patch' || override.op === 'insert') {
|
|
835
|
+
if (override.value === undefined)
|
|
836
|
+
issues.push({ index, slot: override.slot, reason: 'missing_value' });
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (!definition.schema.safeParse(override.value).success)
|
|
840
|
+
issues.push({ index, slot: override.slot, reason: 'invalid_value' });
|
|
841
|
+
});
|
|
842
|
+
return issues;
|
|
843
|
+
}
|
|
844
|
+
export function mvtApplyOverride(base, override) {
|
|
845
|
+
const value = override.value;
|
|
846
|
+
switch (override.op) {
|
|
847
|
+
case 'set':
|
|
848
|
+
return value;
|
|
849
|
+
case 'merge':
|
|
850
|
+
if (base === null || typeof base !== 'object' || Array.isArray(base))
|
|
851
|
+
return base;
|
|
852
|
+
if (value === null || typeof value !== 'object')
|
|
853
|
+
return base;
|
|
854
|
+
return { ...base, ...value };
|
|
855
|
+
case 'remove': {
|
|
856
|
+
if (!Array.isArray(base))
|
|
857
|
+
return base;
|
|
858
|
+
const key = override.match?.by;
|
|
859
|
+
const equals = override.match?.equals;
|
|
860
|
+
if (!key)
|
|
861
|
+
return base;
|
|
862
|
+
return base.filter(item => String(item?.[key] ?? item) !== equals);
|
|
863
|
+
}
|
|
864
|
+
case 'reorder': {
|
|
865
|
+
if (!Array.isArray(base) || !Array.isArray(value))
|
|
866
|
+
return base;
|
|
867
|
+
const key = override.match?.by;
|
|
868
|
+
const identity = (item) => key ? String(item?.[key] ?? '') : String(item);
|
|
869
|
+
const wanted = value.map(String);
|
|
870
|
+
const known = base.filter(item => wanted.includes(identity(item)));
|
|
871
|
+
const rest = base.filter(item => !wanted.includes(identity(item)));
|
|
872
|
+
known.sort((a, b) => wanted.indexOf(identity(a)) - wanted.indexOf(identity(b)));
|
|
873
|
+
return [...known, ...rest];
|
|
874
|
+
}
|
|
875
|
+
case 'insert': {
|
|
876
|
+
if (!Array.isArray(base) || value === undefined)
|
|
877
|
+
return base;
|
|
878
|
+
const next = [...base];
|
|
879
|
+
const at = Math.min(override.at ?? next.length, next.length);
|
|
880
|
+
next.splice(at, 0, value);
|
|
881
|
+
return next;
|
|
882
|
+
}
|
|
883
|
+
case 'patch': {
|
|
884
|
+
if (!Array.isArray(base) || value === null || typeof value !== 'object')
|
|
885
|
+
return base;
|
|
886
|
+
const key = override.match?.by;
|
|
887
|
+
const equals = override.match?.equals;
|
|
888
|
+
if (!key)
|
|
889
|
+
return base;
|
|
890
|
+
return base.map(item => {
|
|
891
|
+
const current = item;
|
|
892
|
+
if (String(current?.[key] ?? '') !== equals)
|
|
893
|
+
return item;
|
|
894
|
+
return { ...current, ...value };
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
default:
|
|
898
|
+
return base;
|
|
899
|
+
}
|
|
900
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lifeaz/lors-helper",
|
|
3
3
|
"description": "Lifeaz Online Retail Storefront Helper",
|
|
4
|
-
"version": "1.0
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"lifeaz": {
|
|
52
52
|
"version": {
|
|
53
53
|
"short": 1,
|
|
54
|
-
"full": "1.0
|
|
55
|
-
"code": "
|
|
54
|
+
"full": "1.1.0-b35",
|
|
55
|
+
"code": "101000035"
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
}
|