@openlettermarketing/olc-react-sdk 2.1.6-beta.1 → 2.1.6-beta.3

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.
@@ -1,11 +1,12 @@
1
1
  import React from 'react';
2
- import { InvalidField } from '../../../utils/template-builder';
3
2
  import './styles.scss';
4
3
  interface FieldValidationModalProps {
5
4
  open: boolean;
6
- invalidFields: InvalidField[];
5
+ invalidFields: string[];
7
6
  currentTheme?: string | null | undefined;
7
+ loading?: boolean;
8
8
  handleClose: () => void;
9
+ handleContinue: () => void;
9
10
  }
10
11
  declare const FieldValidationModal: React.FC<FieldValidationModalProps>;
11
12
  export default FieldValidationModal;
@@ -138,10 +138,10 @@ export declare const MESSAGES: {
138
138
  readonly SUBMIT_BUTTON: "Save";
139
139
  readonly FIELD_VALIDATION: {
140
140
  readonly TITLE: "Invalid Fields";
141
- readonly HEADING: "Please fix the following field errors";
141
+ readonly HEADING: "The following fields are not recognized";
142
142
  readonly DESCRIPTION: (fieldNames: string) => string;
143
- readonly INVALID_FORMAT: "is incorrect";
144
- readonly NOT_UPPERCASE: "is incorrect";
143
+ readonly CONTINUE_BUTTON: "Continue Saving";
144
+ readonly CANCEL_BUTTON: "Cancel";
145
145
  };
146
146
  };
147
147
  readonly QR_CODE_MODAL: {
@@ -88,3 +88,13 @@ export interface InvalidField {
88
88
  * @returns Array of objects containing invalid fields and their issues
89
89
  */
90
90
  export declare const validateTemplateFields: (pages: any) => InvalidField[];
91
+ /**
92
+ * Validates that all dynamic field tokens used in the template exist in the allowed keys list.
93
+ * Scans the entire serialized template JSON for {{...}} patterns and returns any tokens
94
+ * that are not present in the provided allowedKeys list.
95
+ *
96
+ * @param templateJSON - Full template JSON object from store.toJSON()
97
+ * @param allowedKeys - Array of allowed {{...}} token strings (predefined + custom fields)
98
+ * @returns Array of unrecognized token strings; empty array means all fields are valid
99
+ */
100
+ export declare const validateAllowedTemplateFields: (templateJSON: any, allowedKeys: string[]) => string[];
@@ -1 +1 @@
1
- export const SDK_VERSION: "2.1.6-beta.1";
1
+ export const SDK_VERSION: "2.1.6-beta.3";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@openlettermarketing/olc-react-sdk",
3
3
  "private": false,
4
- "version": "2.1.6-beta.1",
4
+ "version": "2.1.6-beta.3",
5
5
  "type": "module",
6
6
  "description": "Simplify template builder integration for any product.",
7
7
  "main": "build/index.js",
@@ -10,17 +10,16 @@ import DialogV2 from '../../GenericUIBlocks/Dialog/V2';
10
10
  // Icons
11
11
  import Warning from '../../../assets/images/modal-icons/warning';
12
12
 
13
- // Types
14
- import { InvalidField } from '../../../utils/template-builder';
15
-
16
13
  // Styles
17
14
  import './styles.scss';
18
15
 
19
16
  interface FieldValidationModalProps {
20
17
  open: boolean;
21
- invalidFields: InvalidField[];
18
+ invalidFields: string[];
22
19
  currentTheme?: string | null | undefined;
20
+ loading?: boolean;
23
21
  handleClose: () => void;
22
+ handleContinue: () => void;
24
23
  }
25
24
 
26
25
  const modalStyles = {
@@ -38,10 +37,11 @@ const FieldValidationModal: React.FC<FieldValidationModalProps> = ({
38
37
  open,
39
38
  invalidFields,
40
39
  currentTheme,
40
+ loading = false,
41
41
  handleClose,
42
+ handleContinue,
42
43
  }) => {
43
- // Build the description with all field names (reversed so UI order matches template order)
44
- const fieldNames = [...invalidFields].reverse().map(item => `"${item.field}"`).join(', ');
44
+ const fieldNames = [...invalidFields].map(item => `"${item}"`).join(', ');
45
45
  const description = MESSAGES.TEMPLATE.FIELD_VALIDATION.DESCRIPTION(fieldNames);
46
46
 
47
47
  return currentTheme === 'v2' ? (
@@ -49,23 +49,32 @@ const FieldValidationModal: React.FC<FieldValidationModalProps> = ({
49
49
  icon={<Warning fill="var(--primary-color)" />}
50
50
  customStyles={modalStylesV2}
51
51
  open={open}
52
+ loading={loading}
52
53
  handleClose={handleClose}
53
54
  title={MESSAGES.TEMPLATE.FIELD_VALIDATION.TITLE}
54
55
  subHeading=""
55
56
  description={description}
56
57
  currentTheme="v2"
57
58
  isGallery={false}
58
- hideButtons={true}
59
+ onSubmit={handleContinue}
60
+ submitText={MESSAGES.TEMPLATE.FIELD_VALIDATION.CONTINUE_BUTTON}
61
+ onCancel={handleClose}
62
+ cancelText={MESSAGES.TEMPLATE.FIELD_VALIDATION.CANCEL_BUTTON}
59
63
  />
60
64
  ) : (
61
65
  <Dialog
62
66
  icon={<Warning fill="var(--primary-color)" />}
63
67
  customStyles={modalStyles}
64
68
  open={open}
69
+ loading={loading}
65
70
  handleClose={handleClose}
66
71
  title={MESSAGES.TEMPLATE.FIELD_VALIDATION.TITLE}
67
72
  subHeading=""
68
73
  description={description}
74
+ onSubmit={handleContinue}
75
+ submitText={MESSAGES.TEMPLATE.FIELD_VALIDATION.CONTINUE_BUTTON}
76
+ onCancel={handleClose}
77
+ cancelText={MESSAGES.TEMPLATE.FIELD_VALIDATION.CANCEL_BUTTON}
69
78
  />
70
79
  );
71
80
  };
@@ -33,7 +33,7 @@ import {
33
33
  validateEmoji,
34
34
  validateGSV,
35
35
  validateTemplateFields,
36
- InvalidField,
36
+ validateAllowedTemplateFields,
37
37
  } from '../../utils/template-builder';
38
38
  import { addSafetyBordersToNonWindowProfessioanl } from '../../utils/templateSafetyBorders/professional';
39
39
  import { addIdentifierAreaToProfessionalNonWindow } from '../../utils/templateIdentifierArea/professional';
@@ -145,7 +145,7 @@ const TopNavigation: React.FC<TopNavigationProps> = ({
145
145
  const [downloadingProof, setDownloaingProof] = useState<boolean>(false);
146
146
  const [downloadingEnvelope, setDownloaingEnvelope] = useState<boolean>(false);
147
147
  const [templateTitle, setTemplateTitle] = useState('');
148
- const [invalidFields, setInvalidFields] = useState<InvalidField[]>([]);
148
+ const [invalidFields, setInvalidFields] = useState<string[]>([]);
149
149
 
150
150
  const { id } = useParams<{ id: string }>();
151
151
 
@@ -451,19 +451,6 @@ const TopNavigation: React.FC<TopNavigationProps> = ({
451
451
  return;
452
452
  }
453
453
 
454
- // Validate fields FIRST - before other validations
455
- const fieldValidationResult = validateTemplateFields(jsonData.pages);
456
-
457
- if (fieldValidationResult.length > 0) {
458
- setInvalidFields(fieldValidationResult);
459
- setIsShowModel((prev) => ({
460
- ...prev,
461
- open: true,
462
- model: 'field-validation'
463
- }));
464
- return; // Stop save process
465
- }
466
-
467
454
  const hasEmoji = validateEmoji(jsonData.pages);
468
455
 
469
456
  if (hasEmoji) {
@@ -589,28 +576,61 @@ const TopNavigation: React.FC<TopNavigationProps> = ({
589
576
  }
590
577
  };
591
578
 
579
+ const handleContinueAnyway = () => {
580
+ setIsShowModel((prev) => ({ ...prev, loading: true }));
581
+ handleSave();
582
+ };
583
+
592
584
  const handleChangeModel = (
593
585
  model: string = '',
594
586
  loading: string | null = null
595
587
  ) => {
596
- // If trying to open save modal, validate fields first
588
+ // When opening the save modal, validate all {{...}} tokens against the allowed list
589
+ // and also catch any malformed/incomplete brace patterns (e.g. {{C.ZIP_COD without closing }})
597
590
  if (model === 'save' && templateType === 'json') {
591
+ const tokenPattern = /\{\{[^{}]+\}\}/g;
592
+
593
+ // Flatten v2 custom field sections (each section has a .fields array)
594
+ const flattenedCustomFieldsV2 = (customFieldsV2 as any[]).length > 0
595
+ ? (customFieldsV2 as any[]).flatMap((section: { fields: any }) => section.fields)
596
+ : [];
597
+
598
+ const allAllowedFields = [
599
+ ...defaultFields,
600
+ ...customFields, // v1 custom fields from API
601
+ ...flattenedCustomFieldsV2, // v2 custom fields from API (flattened)
602
+ ...Object.values(dynamicFields),
603
+ ...defaultSenderFields,
604
+ ...defaultPropertyFields,
605
+ ...defaultMiscFields,
606
+ ];
607
+ const allowedKeys = Array.from(new Set(
608
+ allAllowedFields.flatMap((field: any) =>
609
+ (field?.key || '').match(tokenPattern) || []
610
+ )
611
+ ));
612
+
598
613
  const jsonData = store.toJSON();
599
- const fieldValidationResult = validateTemplateFields(jsonData.pages);
600
-
601
- if (fieldValidationResult.length > 0) {
602
- // Show validation modal instead of save modal
603
- setInvalidFields(fieldValidationResult);
614
+
615
+ // Catch complete tokens not in the allowed list
616
+ const unrecognizedFields = validateAllowedTemplateFields(jsonData, allowedKeys);
617
+
618
+ // Catch malformed/incomplete patterns (e.g. {{C.ZIP_COD with no closing }})
619
+ const formatErrors = validateTemplateFields(jsonData.pages).map((f) => f.field);
620
+
621
+ const allInvalid = Array.from(new Set([...unrecognizedFields, ...formatErrors]));
622
+
623
+ if (allInvalid.length > 0) {
624
+ setInvalidFields(allInvalid);
604
625
  setIsShowModel({
605
626
  open: true,
606
627
  model: 'field-validation',
607
- loading: false
628
+ loading: false,
608
629
  });
609
- return; // Don't open save modal
630
+ return;
610
631
  }
611
632
  }
612
-
613
- // If validation passed or not save modal, proceed normally
633
+
614
634
  setIsShowModel((prev) => ({
615
635
  ...prev,
616
636
  open: !prev.open,
@@ -953,8 +973,10 @@ const TopNavigation: React.FC<TopNavigationProps> = ({
953
973
  <FieldValidationModal
954
974
  open={isShowModel.open}
955
975
  invalidFields={invalidFields}
976
+ loading={isShowModel.loading}
956
977
  currentTheme={currentTheme}
957
978
  handleClose={() => handleChangeModel()}
979
+ handleContinue={handleContinueAnyway}
958
980
  />
959
981
  )}
960
982
  {/* Duplicate Template Modal */}
@@ -145,10 +145,10 @@ export const MESSAGES = {
145
145
  SUBMIT_BUTTON: "Save",
146
146
  FIELD_VALIDATION: {
147
147
  TITLE: "Invalid Fields",
148
- HEADING: "Please fix the following field errors",
149
- DESCRIPTION: (fieldNames: string) => `Following fields ${fieldNames} are incorrect to proceed further. You need to fix those`,
150
- INVALID_FORMAT: "is incorrect",
151
- NOT_UPPERCASE: "is incorrect",
148
+ HEADING: "The following fields are not recognized",
149
+ DESCRIPTION: (fieldNames: string) => `The following field(s) ${fieldNames} are not recognized.`,
150
+ CONTINUE_BUTTON: "Continue Saving",
151
+ CANCEL_BUTTON: "Cancel",
152
152
  },
153
153
  },
154
154
  QR_CODE_MODAL: {
@@ -350,34 +350,53 @@ export const validateTemplateFields = (pages: any): InvalidField[] => {
350
350
  page.children?.forEach((child: any) => {
351
351
  if (child.type === 'text' && child.text) {
352
352
  const text = child.text;
353
-
354
- // Find all potential field tokens/fields in the text.
355
- // A tokentokens/fields is any sequence that contains at least one brace character.
356
- const tokens = text.split(/\s+/);
357
-
358
- tokens.forEach((token: string) => {
359
- if (!token) return;
360
-
361
- // Only process tokens that contain at least one brace
362
- if (!token.includes('{') && !token.includes('}')) return;
363
-
364
- // Valid format: must be EXACTLY {{UPPERCASE_CONTENT}} with no extra chars
353
+
354
+ // ── Step 1 - Find ALL complete brace expressions: anything from the first { to the last } on the same "group", including spaces inside.
355
+ const completePattern = /\{+[^{}]*\}+/g;
356
+ let match: RegExpExecArray | null;
357
+
358
+ // Track which character ranges are already covered so Steps 2 & 3 - don't double-report sub-strings of a complete match.
359
+ const coveredRanges: Array<[number, number]> = [];
360
+
361
+ const completeExec = new RegExp(completePattern.source, 'g');
362
+ while ((match = completeExec.exec(text)) !== null) {
363
+ coveredRanges.push([match.index, match.index + match[0].length]);
364
+ const token = match[0];
365
+ // Valid: EXACTLY {{UPPERCASE_CONTENT}} — no spaces, no lowercase
365
366
  const isValidFormat = /^\{\{[A-Z0-9._]+\}\}$/.test(token);
366
-
367
+
367
368
  if (!isValidFormat) {
368
369
  const content = token.replace(/^\{+|\}+$/g, '');
369
- const isDoubleBrace = /^\{\{.+\}\}$/.test(token);
370
+ const isDoubleBrace = /^\{\{[\s\S]+\}\}$/.test(token);
370
371
  const hasOnlyValidChars = /^[A-Za-z0-9._]+$/.test(content);
371
-
372
372
  if (isDoubleBrace && hasOnlyValidChars && content !== content.toUpperCase()) {
373
- // Double brace but lowercase content
373
+ // e.g. {{c.last_name}} — correct braces but lowercase content
374
374
  invalidFields.push({ field: token, issue: 'NOT_UPPERCASE', pageIndex });
375
375
  } else {
376
- // Any other invalid format
377
376
  invalidFields.push({ field: token, issue: 'INVALID_FORMAT', pageIndex });
378
377
  }
379
378
  }
380
- });
379
+ }
380
+
381
+ // Helper: returns true if the character at `idx` is inside a covered range
382
+ const isCovered = (idx: number): boolean =>
383
+ coveredRanges.some(([s, e]) => idx >= s && idx < e);
384
+
385
+ // ── Step 2 ─ Find incomplete patterns that have opening brace(s) but NO closing brace.e.g. "{C.LAST_NAME" or "{{C.LAST_NAME" at end of text.
386
+ const missingClosingExec = /\{+[^{}]*/g;
387
+ while ((match = missingClosingExec.exec(text)) !== null) {
388
+ if (!isCovered(match.index) && match[0].trim().length > 0) {
389
+ invalidFields.push({ field: match[0], issue: 'INVALID_FORMAT', pageIndex });
390
+ }
391
+ }
392
+
393
+ // ── Step 3 ─ Find incomplete patterns that have closing brace(s) but NO opening brace. e.g. "C.LAST_NAME}" or "C.LAST_NAME}}".
394
+ const missingOpeningExec = /(?<!\{)[A-Z0-9][A-Z0-9._\s]*\}+/g;
395
+ while ((match = missingOpeningExec.exec(text)) !== null) {
396
+ if (!isCovered(match.index) && match[0].trim().length > 0) {
397
+ invalidFields.push({ field: match[0].trim(), issue: 'INVALID_FORMAT', pageIndex });
398
+ }
399
+ }
381
400
  }
382
401
  });
383
402
  });
@@ -389,3 +408,23 @@ export const validateTemplateFields = (pages: any): InvalidField[] => {
389
408
 
390
409
  return uniqueFields;
391
410
  };
411
+
412
+ /**
413
+ * Validates that all dynamic field tokens used in the template exist in the allowed keys list.
414
+ * Scans the entire serialized template JSON for {{...}} patterns and returns any tokens
415
+ * that are not present in the provided allowedKeys list.
416
+ *
417
+ * @param templateJSON - Full template JSON object from store.toJSON()
418
+ * @param allowedKeys - Array of allowed {{...}} token strings (predefined + custom fields)
419
+ * @returns Array of unrecognized token strings; empty array means all fields are valid
420
+ */
421
+ export const validateAllowedTemplateFields = (
422
+ templateJSON: any,
423
+ allowedKeys: string[]
424
+ ): string[] => {
425
+ const templateStr = JSON.stringify(templateJSON);
426
+ const pattern = /\{\{[^{}]+\}\}/g;
427
+ const matches = templateStr.match(pattern) || [];
428
+ const unique = Array.from(new Set(matches));
429
+ return unique.filter(token => !allowedKeys.includes(token));
430
+ };
package/version.js CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = '2.1.6-beta.1';
1
+ export const SDK_VERSION = '2.1.6-beta.3';