@redacto.io/consent-sdk-react 3.1.0 → 3.2.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,5 +1,11 @@
1
1
  # @redacto.io/consent-sdk-react
2
2
 
3
+ ## 3.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - enhance PurposeItem component to track initial data elements
8
+
3
9
  ## 3.1.0
4
10
 
5
11
  ### Minor Changes
package/dist/index.js CHANGED
@@ -899,7 +899,8 @@ var PurposeItem = ({
899
899
  onDataElementToggle,
900
900
  getTranslatedText,
901
901
  isAlreadyConsented,
902
- selectedLanguage
902
+ selectedLanguage,
903
+ initialDataElementSelections = {}
903
904
  }) => {
904
905
  const headingStyle = (0, import_react2.useMemo)(
905
906
  () => ({
@@ -1114,46 +1115,68 @@ var PurposeItem = ({
1114
1115
  )}${dataElement.required ? " (required)" : ""}`
1115
1116
  }
1116
1117
  ),
1117
- isAlreadyConsented && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1118
- "div",
1119
- {
1120
- style: {
1121
- display: "flex",
1122
- alignItems: "center",
1123
- justifyContent: "center",
1124
- width: "16px",
1125
- height: "16px",
1126
- borderRadius: "50%",
1127
- backgroundColor: "#10B981"
1128
- },
1129
- "aria-label": `Already consented to ${getTranslatedText(
1130
- `data_elements.name`,
1131
- dataElement.name,
1132
- dataElement.uuid
1133
- )}`,
1134
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1135
- "svg",
1118
+ isAlreadyConsented && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: (() => {
1119
+ const combinedId = `${purpose.uuid}-${dataElement.uuid}`;
1120
+ const isCurrentlySelected = selectedDataElements[combinedId] || false;
1121
+ const wasOriginallySelected = initialDataElementSelections[combinedId] || false;
1122
+ if (isCurrentlySelected && wasOriginallySelected) {
1123
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1124
+ "div",
1136
1125
  {
1137
- width: "10",
1138
- height: "10",
1139
- viewBox: "0 0 10 10",
1140
- fill: "none",
1141
- xmlns: "http://www.w3.org/2000/svg",
1142
- "aria-hidden": "true",
1126
+ style: {
1127
+ display: "flex",
1128
+ alignItems: "center",
1129
+ justifyContent: "center",
1130
+ width: "16px",
1131
+ height: "16px",
1132
+ borderRadius: "50%",
1133
+ backgroundColor: "#10B981"
1134
+ },
1135
+ "aria-label": `Already consented to ${getTranslatedText(
1136
+ `data_elements.name`,
1137
+ dataElement.name,
1138
+ dataElement.uuid
1139
+ )}`,
1143
1140
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1144
- "path",
1141
+ "svg",
1145
1142
  {
1146
- d: "M3 5L4.5 6.5L7.5 3.5",
1147
- stroke: "white",
1148
- strokeWidth: "1.5",
1149
- strokeLinecap: "round",
1150
- strokeLinejoin: "round"
1143
+ width: "10",
1144
+ height: "10",
1145
+ viewBox: "0 0 10 10",
1146
+ fill: "none",
1147
+ xmlns: "http://www.w3.org/2000/svg",
1148
+ "aria-hidden": "true",
1149
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1150
+ "path",
1151
+ {
1152
+ d: "M3 5L4.5 6.5L7.5 3.5",
1153
+ stroke: "white",
1154
+ strokeWidth: "1.5",
1155
+ strokeLinecap: "round",
1156
+ strokeLinejoin: "round"
1157
+ }
1158
+ )
1151
1159
  }
1152
1160
  )
1153
1161
  }
1154
- )
1162
+ );
1163
+ } else {
1164
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1165
+ "input",
1166
+ {
1167
+ className: "redacto-checkbox-small",
1168
+ type: "checkbox",
1169
+ checked: isCurrentlySelected,
1170
+ onChange: () => onDataElementToggle(dataElement.uuid, purpose.uuid),
1171
+ "aria-label": `Select ${getTranslatedText(
1172
+ `data_elements.name`,
1173
+ dataElement.name,
1174
+ dataElement.uuid
1175
+ )}${dataElement.required ? " (required)" : ""}`
1176
+ }
1177
+ );
1155
1178
  }
1156
- )
1179
+ })() })
1157
1180
  ]
1158
1181
  },
1159
1182
  `${dataElement.uuid}-${selectedLanguage}`
@@ -1592,10 +1615,13 @@ var RedactoNoticeConsent = ({
1592
1615
  }
1593
1616
  const [isLoading, setIsLoading] = (0, import_react2.useState)(true);
1594
1617
  const [isSubmitting, setIsSubmitting] = (0, import_react2.useState)(false);
1618
+ const [isSubmittingAccept, setIsSubmittingAccept] = (0, import_react2.useState)(false);
1619
+ const [isSubmittingDecline, setIsSubmittingDecline] = (0, import_react2.useState)(false);
1595
1620
  const [content, setContent] = (0, import_react2.useState)(null);
1596
1621
  const [categorizedPurposes, setCategorizedPurposes] = (0, import_react2.useState)(null);
1597
1622
  const [selectedPurposes, setSelectedPurposes] = (0, import_react2.useState)({});
1598
1623
  const [selectedDataElements, setSelectedDataElements] = (0, import_react2.useState)({});
1624
+ const [initialDataElementSelections, setInitialDataElementSelections] = (0, import_react2.useState)({});
1599
1625
  const [collapsedPurposes, setCollapsedPurposes] = (0, import_react2.useState)({});
1600
1626
  const [hasAlreadyConsented, setHasAlreadyConsented] = (0, import_react2.useState)(false);
1601
1627
  const [isLanguageDropdownOpen, setIsLanguageDropdownOpen] = (0, import_react2.useState)(false);
@@ -1736,11 +1762,14 @@ var RedactoNoticeConsent = ({
1736
1762
  setCategorizedPurposes(null);
1737
1763
  setSelectedPurposes({});
1738
1764
  setSelectedDataElements({});
1765
+ setInitialDataElementSelections({});
1739
1766
  setCollapsedPurposes({});
1740
1767
  setHasAlreadyConsented(false);
1741
1768
  setErrorMessage(null);
1742
1769
  setFetchError(null);
1743
1770
  setIsLoading(true);
1771
+ setIsSubmittingAccept(false);
1772
+ setIsSubmittingDecline(false);
1744
1773
  setShowAgeVerification(false);
1745
1774
  setShowGuardianForm(false);
1746
1775
  setIsSubmittingGuardian(false);
@@ -1883,6 +1912,23 @@ var RedactoNoticeConsent = ({
1883
1912
  {}
1884
1913
  );
1885
1914
  setSelectedDataElements(initialDataElementState);
1915
+ if (isReconsentRequired && newCategorizedPurposes) {
1916
+ const initialSelections = {};
1917
+ newCategorizedPurposes.alreadyConsented.forEach((purpose) => {
1918
+ purpose.data_elements.forEach((element) => {
1919
+ var _a3, _b2;
1920
+ const combinedId = `${purpose.uuid}-${element.uuid}`;
1921
+ if ((_b2 = (_a3 = purposeSelections == null ? void 0 : purposeSelections[purpose.uuid]) == null ? void 0 : _a3.data_elements) == null ? void 0 : _b2[element.uuid]) {
1922
+ initialSelections[combinedId] = purposeSelections[purpose.uuid].data_elements[element.uuid].selected;
1923
+ } else {
1924
+ initialSelections[combinedId] = false;
1925
+ }
1926
+ });
1927
+ });
1928
+ setInitialDataElementSelections(initialSelections);
1929
+ } else {
1930
+ setInitialDataElementSelections({});
1931
+ }
1886
1932
  if (consentContentData.detail.is_minor) {
1887
1933
  setShowAgeVerification(true);
1888
1934
  }
@@ -1983,6 +2029,7 @@ var RedactoNoticeConsent = ({
1983
2029
  const handleAccept = async () => {
1984
2030
  const submitController = new AbortController();
1985
2031
  try {
2032
+ setIsSubmittingAccept(true);
1986
2033
  setIsSubmitting(true);
1987
2034
  setErrorMessage(null);
1988
2035
  if (content) {
@@ -1990,14 +2037,31 @@ var RedactoNoticeConsent = ({
1990
2037
  accessToken,
1991
2038
  baseUrl,
1992
2039
  noticeUuid: content.notice_uuid,
1993
- purposes: content.purposes.map((purpose) => ({
1994
- ...purpose,
1995
- selected: selectedPurposes[purpose.uuid] || false,
1996
- data_elements: purpose.data_elements.map((element) => ({
1997
- ...element,
1998
- selected: element.required ? true : selectedDataElements[`${purpose.uuid}-${element.uuid}`] || false
1999
- }))
2000
- })),
2040
+ purposes: content.purposes.map((purpose) => {
2041
+ const isAlreadyConsented = (categorizedPurposes == null ? void 0 : categorizedPurposes.alreadyConsented.some(
2042
+ (p) => p.uuid === purpose.uuid
2043
+ )) || false;
2044
+ let purposeSelected = selectedPurposes[purpose.uuid] || false;
2045
+ if (isAlreadyConsented) {
2046
+ const wasModified = purpose.data_elements.some((element) => {
2047
+ const combinedId = `${purpose.uuid}-${element.uuid}`;
2048
+ const currentSelection = selectedDataElements[combinedId] || false;
2049
+ const initialSelection = initialDataElementSelections[combinedId] || false;
2050
+ return currentSelection !== initialSelection;
2051
+ });
2052
+ if (wasModified) {
2053
+ purposeSelected = true;
2054
+ }
2055
+ }
2056
+ return {
2057
+ ...purpose,
2058
+ selected: purposeSelected,
2059
+ data_elements: purpose.data_elements.map((element) => ({
2060
+ ...element,
2061
+ selected: element.required ? true : selectedDataElements[`${purpose.uuid}-${element.uuid}`] || false
2062
+ }))
2063
+ };
2064
+ }),
2001
2065
  declined: false,
2002
2066
  meta_data: applicationId ? { specific_uuid: applicationId } : void 0,
2003
2067
  signal: submitController.signal
@@ -2017,11 +2081,19 @@ var RedactoNoticeConsent = ({
2017
2081
  }
2018
2082
  onError == null ? void 0 : onError(error);
2019
2083
  } finally {
2084
+ setIsSubmittingAccept(false);
2020
2085
  setIsSubmitting(false);
2021
2086
  }
2022
2087
  };
2023
2088
  const handleDecline = (0, import_react2.useCallback)(async () => {
2024
- onDecline == null ? void 0 : onDecline();
2089
+ try {
2090
+ setIsSubmittingDecline(true);
2091
+ setIsSubmitting(true);
2092
+ onDecline == null ? void 0 : onDecline();
2093
+ } finally {
2094
+ setIsSubmittingDecline(false);
2095
+ setIsSubmitting(false);
2096
+ }
2025
2097
  }, [onDecline]);
2026
2098
  const handleGuardianFormChange = (0, import_react2.useCallback)(
2027
2099
  (field, value) => {
@@ -2995,7 +3067,8 @@ var RedactoNoticeConsent = ({
2995
3067
  onDataElementToggle: handleDataElementCheckboxChange,
2996
3068
  getTranslatedText,
2997
3069
  isAlreadyConsented: true,
2998
- selectedLanguage
3070
+ selectedLanguage,
3071
+ initialDataElementSelections
2999
3072
  },
3000
3073
  `${purpose.uuid}-${selectedLanguage}`
3001
3074
  )),
@@ -3065,12 +3138,12 @@ var RedactoNoticeConsent = ({
3065
3138
  opacity: acceptDisabled ? 0.5 : 1
3066
3139
  },
3067
3140
  onClick: handleAccept,
3068
- disabled: acceptDisabled,
3141
+ disabled: acceptDisabled || isSubmittingDecline,
3069
3142
  "aria-label": getTranslatedText(
3070
3143
  "confirm_button_text",
3071
3144
  (content == null ? void 0 : content.confirm_button_text) || "Accept"
3072
3145
  ),
3073
- children: isSubmitting ? "Processing..." : getTranslatedText(
3146
+ children: isSubmittingAccept ? "Processing..." : getTranslatedText(
3074
3147
  "confirm_button_text",
3075
3148
  (content == null ? void 0 : content.confirm_button_text) || "Accept"
3076
3149
  )
@@ -3084,15 +3157,16 @@ var RedactoNoticeConsent = ({
3084
3157
  ...styles.button,
3085
3158
  ...responsiveStyles.button,
3086
3159
  ...styles.cancelButton,
3087
- ...declineButtonStyle
3160
+ ...declineButtonStyle,
3161
+ opacity: isSubmittingAccept ? 0.5 : 1
3088
3162
  },
3089
3163
  onClick: handleDecline,
3090
- disabled: isSubmitting,
3164
+ disabled: isSubmittingDecline || isSubmittingAccept,
3091
3165
  "aria-label": getTranslatedText(
3092
3166
  "decline_button_text",
3093
3167
  (content == null ? void 0 : content.decline_button_text) || "Decline"
3094
3168
  ),
3095
- children: isSubmitting ? "Processing..." : getTranslatedText(
3169
+ children: isSubmittingDecline ? "Processing..." : getTranslatedText(
3096
3170
  "decline_button_text",
3097
3171
  (content == null ? void 0 : content.decline_button_text) || "Decline"
3098
3172
  )
package/dist/index.mjs CHANGED
@@ -872,7 +872,8 @@ var PurposeItem = ({
872
872
  onDataElementToggle,
873
873
  getTranslatedText,
874
874
  isAlreadyConsented,
875
- selectedLanguage
875
+ selectedLanguage,
876
+ initialDataElementSelections = {}
876
877
  }) => {
877
878
  const headingStyle = useMemo(
878
879
  () => ({
@@ -1087,46 +1088,68 @@ var PurposeItem = ({
1087
1088
  )}${dataElement.required ? " (required)" : ""}`
1088
1089
  }
1089
1090
  ),
1090
- isAlreadyConsented && /* @__PURE__ */ jsx(
1091
- "div",
1092
- {
1093
- style: {
1094
- display: "flex",
1095
- alignItems: "center",
1096
- justifyContent: "center",
1097
- width: "16px",
1098
- height: "16px",
1099
- borderRadius: "50%",
1100
- backgroundColor: "#10B981"
1101
- },
1102
- "aria-label": `Already consented to ${getTranslatedText(
1103
- `data_elements.name`,
1104
- dataElement.name,
1105
- dataElement.uuid
1106
- )}`,
1107
- children: /* @__PURE__ */ jsx(
1108
- "svg",
1091
+ isAlreadyConsented && /* @__PURE__ */ jsx(Fragment, { children: (() => {
1092
+ const combinedId = `${purpose.uuid}-${dataElement.uuid}`;
1093
+ const isCurrentlySelected = selectedDataElements[combinedId] || false;
1094
+ const wasOriginallySelected = initialDataElementSelections[combinedId] || false;
1095
+ if (isCurrentlySelected && wasOriginallySelected) {
1096
+ return /* @__PURE__ */ jsx(
1097
+ "div",
1109
1098
  {
1110
- width: "10",
1111
- height: "10",
1112
- viewBox: "0 0 10 10",
1113
- fill: "none",
1114
- xmlns: "http://www.w3.org/2000/svg",
1115
- "aria-hidden": "true",
1099
+ style: {
1100
+ display: "flex",
1101
+ alignItems: "center",
1102
+ justifyContent: "center",
1103
+ width: "16px",
1104
+ height: "16px",
1105
+ borderRadius: "50%",
1106
+ backgroundColor: "#10B981"
1107
+ },
1108
+ "aria-label": `Already consented to ${getTranslatedText(
1109
+ `data_elements.name`,
1110
+ dataElement.name,
1111
+ dataElement.uuid
1112
+ )}`,
1116
1113
  children: /* @__PURE__ */ jsx(
1117
- "path",
1114
+ "svg",
1118
1115
  {
1119
- d: "M3 5L4.5 6.5L7.5 3.5",
1120
- stroke: "white",
1121
- strokeWidth: "1.5",
1122
- strokeLinecap: "round",
1123
- strokeLinejoin: "round"
1116
+ width: "10",
1117
+ height: "10",
1118
+ viewBox: "0 0 10 10",
1119
+ fill: "none",
1120
+ xmlns: "http://www.w3.org/2000/svg",
1121
+ "aria-hidden": "true",
1122
+ children: /* @__PURE__ */ jsx(
1123
+ "path",
1124
+ {
1125
+ d: "M3 5L4.5 6.5L7.5 3.5",
1126
+ stroke: "white",
1127
+ strokeWidth: "1.5",
1128
+ strokeLinecap: "round",
1129
+ strokeLinejoin: "round"
1130
+ }
1131
+ )
1124
1132
  }
1125
1133
  )
1126
1134
  }
1127
- )
1135
+ );
1136
+ } else {
1137
+ return /* @__PURE__ */ jsx(
1138
+ "input",
1139
+ {
1140
+ className: "redacto-checkbox-small",
1141
+ type: "checkbox",
1142
+ checked: isCurrentlySelected,
1143
+ onChange: () => onDataElementToggle(dataElement.uuid, purpose.uuid),
1144
+ "aria-label": `Select ${getTranslatedText(
1145
+ `data_elements.name`,
1146
+ dataElement.name,
1147
+ dataElement.uuid
1148
+ )}${dataElement.required ? " (required)" : ""}`
1149
+ }
1150
+ );
1128
1151
  }
1129
- )
1152
+ })() })
1130
1153
  ]
1131
1154
  },
1132
1155
  `${dataElement.uuid}-${selectedLanguage}`
@@ -1565,10 +1588,13 @@ var RedactoNoticeConsent = ({
1565
1588
  }
1566
1589
  const [isLoading, setIsLoading] = useState2(true);
1567
1590
  const [isSubmitting, setIsSubmitting] = useState2(false);
1591
+ const [isSubmittingAccept, setIsSubmittingAccept] = useState2(false);
1592
+ const [isSubmittingDecline, setIsSubmittingDecline] = useState2(false);
1568
1593
  const [content, setContent] = useState2(null);
1569
1594
  const [categorizedPurposes, setCategorizedPurposes] = useState2(null);
1570
1595
  const [selectedPurposes, setSelectedPurposes] = useState2({});
1571
1596
  const [selectedDataElements, setSelectedDataElements] = useState2({});
1597
+ const [initialDataElementSelections, setInitialDataElementSelections] = useState2({});
1572
1598
  const [collapsedPurposes, setCollapsedPurposes] = useState2({});
1573
1599
  const [hasAlreadyConsented, setHasAlreadyConsented] = useState2(false);
1574
1600
  const [isLanguageDropdownOpen, setIsLanguageDropdownOpen] = useState2(false);
@@ -1709,11 +1735,14 @@ var RedactoNoticeConsent = ({
1709
1735
  setCategorizedPurposes(null);
1710
1736
  setSelectedPurposes({});
1711
1737
  setSelectedDataElements({});
1738
+ setInitialDataElementSelections({});
1712
1739
  setCollapsedPurposes({});
1713
1740
  setHasAlreadyConsented(false);
1714
1741
  setErrorMessage(null);
1715
1742
  setFetchError(null);
1716
1743
  setIsLoading(true);
1744
+ setIsSubmittingAccept(false);
1745
+ setIsSubmittingDecline(false);
1717
1746
  setShowAgeVerification(false);
1718
1747
  setShowGuardianForm(false);
1719
1748
  setIsSubmittingGuardian(false);
@@ -1856,6 +1885,23 @@ var RedactoNoticeConsent = ({
1856
1885
  {}
1857
1886
  );
1858
1887
  setSelectedDataElements(initialDataElementState);
1888
+ if (isReconsentRequired && newCategorizedPurposes) {
1889
+ const initialSelections = {};
1890
+ newCategorizedPurposes.alreadyConsented.forEach((purpose) => {
1891
+ purpose.data_elements.forEach((element) => {
1892
+ var _a3, _b2;
1893
+ const combinedId = `${purpose.uuid}-${element.uuid}`;
1894
+ if ((_b2 = (_a3 = purposeSelections == null ? void 0 : purposeSelections[purpose.uuid]) == null ? void 0 : _a3.data_elements) == null ? void 0 : _b2[element.uuid]) {
1895
+ initialSelections[combinedId] = purposeSelections[purpose.uuid].data_elements[element.uuid].selected;
1896
+ } else {
1897
+ initialSelections[combinedId] = false;
1898
+ }
1899
+ });
1900
+ });
1901
+ setInitialDataElementSelections(initialSelections);
1902
+ } else {
1903
+ setInitialDataElementSelections({});
1904
+ }
1859
1905
  if (consentContentData.detail.is_minor) {
1860
1906
  setShowAgeVerification(true);
1861
1907
  }
@@ -1956,6 +2002,7 @@ var RedactoNoticeConsent = ({
1956
2002
  const handleAccept = async () => {
1957
2003
  const submitController = new AbortController();
1958
2004
  try {
2005
+ setIsSubmittingAccept(true);
1959
2006
  setIsSubmitting(true);
1960
2007
  setErrorMessage(null);
1961
2008
  if (content) {
@@ -1963,14 +2010,31 @@ var RedactoNoticeConsent = ({
1963
2010
  accessToken,
1964
2011
  baseUrl,
1965
2012
  noticeUuid: content.notice_uuid,
1966
- purposes: content.purposes.map((purpose) => ({
1967
- ...purpose,
1968
- selected: selectedPurposes[purpose.uuid] || false,
1969
- data_elements: purpose.data_elements.map((element) => ({
1970
- ...element,
1971
- selected: element.required ? true : selectedDataElements[`${purpose.uuid}-${element.uuid}`] || false
1972
- }))
1973
- })),
2013
+ purposes: content.purposes.map((purpose) => {
2014
+ const isAlreadyConsented = (categorizedPurposes == null ? void 0 : categorizedPurposes.alreadyConsented.some(
2015
+ (p) => p.uuid === purpose.uuid
2016
+ )) || false;
2017
+ let purposeSelected = selectedPurposes[purpose.uuid] || false;
2018
+ if (isAlreadyConsented) {
2019
+ const wasModified = purpose.data_elements.some((element) => {
2020
+ const combinedId = `${purpose.uuid}-${element.uuid}`;
2021
+ const currentSelection = selectedDataElements[combinedId] || false;
2022
+ const initialSelection = initialDataElementSelections[combinedId] || false;
2023
+ return currentSelection !== initialSelection;
2024
+ });
2025
+ if (wasModified) {
2026
+ purposeSelected = true;
2027
+ }
2028
+ }
2029
+ return {
2030
+ ...purpose,
2031
+ selected: purposeSelected,
2032
+ data_elements: purpose.data_elements.map((element) => ({
2033
+ ...element,
2034
+ selected: element.required ? true : selectedDataElements[`${purpose.uuid}-${element.uuid}`] || false
2035
+ }))
2036
+ };
2037
+ }),
1974
2038
  declined: false,
1975
2039
  meta_data: applicationId ? { specific_uuid: applicationId } : void 0,
1976
2040
  signal: submitController.signal
@@ -1990,11 +2054,19 @@ var RedactoNoticeConsent = ({
1990
2054
  }
1991
2055
  onError == null ? void 0 : onError(error);
1992
2056
  } finally {
2057
+ setIsSubmittingAccept(false);
1993
2058
  setIsSubmitting(false);
1994
2059
  }
1995
2060
  };
1996
2061
  const handleDecline = useCallback(async () => {
1997
- onDecline == null ? void 0 : onDecline();
2062
+ try {
2063
+ setIsSubmittingDecline(true);
2064
+ setIsSubmitting(true);
2065
+ onDecline == null ? void 0 : onDecline();
2066
+ } finally {
2067
+ setIsSubmittingDecline(false);
2068
+ setIsSubmitting(false);
2069
+ }
1998
2070
  }, [onDecline]);
1999
2071
  const handleGuardianFormChange = useCallback(
2000
2072
  (field, value) => {
@@ -2968,7 +3040,8 @@ var RedactoNoticeConsent = ({
2968
3040
  onDataElementToggle: handleDataElementCheckboxChange,
2969
3041
  getTranslatedText,
2970
3042
  isAlreadyConsented: true,
2971
- selectedLanguage
3043
+ selectedLanguage,
3044
+ initialDataElementSelections
2972
3045
  },
2973
3046
  `${purpose.uuid}-${selectedLanguage}`
2974
3047
  )),
@@ -3038,12 +3111,12 @@ var RedactoNoticeConsent = ({
3038
3111
  opacity: acceptDisabled ? 0.5 : 1
3039
3112
  },
3040
3113
  onClick: handleAccept,
3041
- disabled: acceptDisabled,
3114
+ disabled: acceptDisabled || isSubmittingDecline,
3042
3115
  "aria-label": getTranslatedText(
3043
3116
  "confirm_button_text",
3044
3117
  (content == null ? void 0 : content.confirm_button_text) || "Accept"
3045
3118
  ),
3046
- children: isSubmitting ? "Processing..." : getTranslatedText(
3119
+ children: isSubmittingAccept ? "Processing..." : getTranslatedText(
3047
3120
  "confirm_button_text",
3048
3121
  (content == null ? void 0 : content.confirm_button_text) || "Accept"
3049
3122
  )
@@ -3057,15 +3130,16 @@ var RedactoNoticeConsent = ({
3057
3130
  ...styles.button,
3058
3131
  ...responsiveStyles.button,
3059
3132
  ...styles.cancelButton,
3060
- ...declineButtonStyle
3133
+ ...declineButtonStyle,
3134
+ opacity: isSubmittingAccept ? 0.5 : 1
3061
3135
  },
3062
3136
  onClick: handleDecline,
3063
- disabled: isSubmitting,
3137
+ disabled: isSubmittingDecline || isSubmittingAccept,
3064
3138
  "aria-label": getTranslatedText(
3065
3139
  "decline_button_text",
3066
3140
  (content == null ? void 0 : content.decline_button_text) || "Decline"
3067
3141
  ),
3068
- children: isSubmitting ? "Processing..." : getTranslatedText(
3142
+ children: isSubmittingDecline ? "Processing..." : getTranslatedText(
3069
3143
  "decline_button_text",
3070
3144
  (content == null ? void 0 : content.decline_button_text) || "Decline"
3071
3145
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redacto.io/consent-sdk-react",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "main": "dist/index.cjs",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -45,7 +45,10 @@ import { useMediaQuery } from "./useMediaQuery";
45
45
  * Supports both regular consent and reconsent UI modes.
46
46
  */
47
47
  const PurposeItem: React.FC<
48
- PurposeItemProps & { selectedLanguage: string }
48
+ PurposeItemProps & {
49
+ selectedLanguage: string;
50
+ initialDataElementSelections?: Record<string, boolean>;
51
+ }
49
52
  > = ({
50
53
  purpose,
51
54
  selectedPurposes,
@@ -60,6 +63,7 @@ const PurposeItem: React.FC<
60
63
  getTranslatedText,
61
64
  isAlreadyConsented,
62
65
  selectedLanguage,
66
+ initialDataElementSelections = {},
63
67
  }) => {
64
68
  const headingStyle = useMemo(
65
69
  () => ({
@@ -266,39 +270,71 @@ const PurposeItem: React.FC<
266
270
  />
267
271
  )}
268
272
  {isAlreadyConsented && (
269
- <div
270
- style={{
271
- display: "flex",
272
- alignItems: "center",
273
- justifyContent: "center",
274
- width: "16px",
275
- height: "16px",
276
- borderRadius: "50%",
277
- backgroundColor: "#10B981",
278
- }}
279
- aria-label={`Already consented to ${getTranslatedText(
280
- `data_elements.name`,
281
- dataElement.name,
282
- dataElement.uuid
283
- )}`}
284
- >
285
- <svg
286
- width="10"
287
- height="10"
288
- viewBox="0 0 10 10"
289
- fill="none"
290
- xmlns="http://www.w3.org/2000/svg"
291
- aria-hidden="true"
292
- >
293
- <path
294
- d="M3 5L4.5 6.5L7.5 3.5"
295
- stroke="white"
296
- strokeWidth="1.5"
297
- strokeLinecap="round"
298
- strokeLinejoin="round"
299
- />
300
- </svg>
301
- </div>
273
+ <>
274
+ {(() => {
275
+ const combinedId = `${purpose.uuid}-${dataElement.uuid}`;
276
+ const isCurrentlySelected =
277
+ selectedDataElements[combinedId] || false;
278
+ const wasOriginallySelected =
279
+ initialDataElementSelections[combinedId] || false;
280
+
281
+ // Show green tick only if it was originally selected
282
+ // Show checkbox if it's newly selected or unselected
283
+ if (isCurrentlySelected && wasOriginallySelected) {
284
+ return (
285
+ <div
286
+ style={{
287
+ display: "flex",
288
+ alignItems: "center",
289
+ justifyContent: "center",
290
+ width: "16px",
291
+ height: "16px",
292
+ borderRadius: "50%",
293
+ backgroundColor: "#10B981",
294
+ }}
295
+ aria-label={`Already consented to ${getTranslatedText(
296
+ `data_elements.name`,
297
+ dataElement.name,
298
+ dataElement.uuid
299
+ )}`}
300
+ >
301
+ <svg
302
+ width="10"
303
+ height="10"
304
+ viewBox="0 0 10 10"
305
+ fill="none"
306
+ xmlns="http://www.w3.org/2000/svg"
307
+ aria-hidden="true"
308
+ >
309
+ <path
310
+ d="M3 5L4.5 6.5L7.5 3.5"
311
+ stroke="white"
312
+ strokeWidth="1.5"
313
+ strokeLinecap="round"
314
+ strokeLinejoin="round"
315
+ />
316
+ </svg>
317
+ </div>
318
+ );
319
+ } else {
320
+ return (
321
+ <input
322
+ className="redacto-checkbox-small"
323
+ type="checkbox"
324
+ checked={isCurrentlySelected}
325
+ onChange={() =>
326
+ onDataElementToggle(dataElement.uuid, purpose.uuid)
327
+ }
328
+ aria-label={`Select ${getTranslatedText(
329
+ `data_elements.name`,
330
+ dataElement.name,
331
+ dataElement.uuid
332
+ )}${dataElement.required ? " (required)" : ""}`}
333
+ />
334
+ );
335
+ }
336
+ })()}
337
+ </>
302
338
  )}
303
339
  </div>
304
340
  ))}
@@ -776,6 +812,8 @@ export const RedactoNoticeConsent = ({
776
812
  // Loading and submission states
777
813
  const [isLoading, setIsLoading] = useState(true);
778
814
  const [isSubmitting, setIsSubmitting] = useState(false);
815
+ const [isSubmittingAccept, setIsSubmittingAccept] = useState(false);
816
+ const [isSubmittingDecline, setIsSubmittingDecline] = useState(false);
779
817
 
780
818
  // Content and consent data
781
819
  const [content, setContent] = useState<
@@ -793,6 +831,9 @@ export const RedactoNoticeConsent = ({
793
831
  const [selectedDataElements, setSelectedDataElements] = useState<
794
832
  Record<string, boolean>
795
833
  >({});
834
+ // Track initial data element selections for already-consented purposes
835
+ const [initialDataElementSelections, setInitialDataElementSelections] =
836
+ useState<Record<string, boolean>>({});
796
837
  const [collapsedPurposes, setCollapsedPurposes] = useState<
797
838
  Record<string, boolean>
798
839
  >({});
@@ -987,11 +1028,14 @@ export const RedactoNoticeConsent = ({
987
1028
  setCategorizedPurposes(null);
988
1029
  setSelectedPurposes({});
989
1030
  setSelectedDataElements({});
1031
+ setInitialDataElementSelections({});
990
1032
  setCollapsedPurposes({});
991
1033
  setHasAlreadyConsented(false);
992
1034
  setErrorMessage(null);
993
1035
  setFetchError(null);
994
1036
  setIsLoading(true);
1037
+ setIsSubmittingAccept(false);
1038
+ setIsSubmittingDecline(false);
995
1039
 
996
1040
  // Clear age verification and guardian form state
997
1041
  setShowAgeVerification(false);
@@ -1213,6 +1257,27 @@ export const RedactoNoticeConsent = ({
1213
1257
  {}
1214
1258
  );
1215
1259
  setSelectedDataElements(initialDataElementState);
1260
+ // Store initial selections for already-consented purposes to track modifications
1261
+ if (isReconsentRequired && newCategorizedPurposes) {
1262
+ const initialSelections: Record<string, boolean> = {};
1263
+ newCategorizedPurposes.alreadyConsented.forEach((purpose) => {
1264
+ purpose.data_elements.forEach((element) => {
1265
+ const combinedId = `${purpose.uuid}-${element.uuid}`;
1266
+ if (
1267
+ purposeSelections?.[purpose.uuid]?.data_elements?.[element.uuid]
1268
+ ) {
1269
+ initialSelections[combinedId] =
1270
+ purposeSelections[purpose.uuid].data_elements[element.uuid]
1271
+ .selected;
1272
+ } else {
1273
+ initialSelections[combinedId] = false;
1274
+ }
1275
+ });
1276
+ });
1277
+ setInitialDataElementSelections(initialSelections);
1278
+ } else {
1279
+ setInitialDataElementSelections({});
1280
+ }
1216
1281
 
1217
1282
  // Check if age verification is required - set this after all content is loaded
1218
1283
  // but before setIsLoading(false) to ensure proper render order
@@ -1350,24 +1415,52 @@ export const RedactoNoticeConsent = ({
1350
1415
  const submitController = new AbortController();
1351
1416
 
1352
1417
  try {
1353
- setIsSubmitting(true);
1418
+ setIsSubmittingAccept(true);
1419
+ setIsSubmitting(true); // Keep for backward compatibility with acceptDisabled
1354
1420
  setErrorMessage(null);
1355
1421
  if (content) {
1356
1422
  await submitConsentEvent({
1357
1423
  accessToken,
1358
1424
  baseUrl,
1359
1425
  noticeUuid: content.notice_uuid,
1360
- purposes: content.purposes.map((purpose) => ({
1361
- ...purpose,
1362
- selected: selectedPurposes[purpose.uuid] || false,
1363
- data_elements: purpose.data_elements.map((element) => ({
1364
- ...element,
1365
- selected: element.required
1366
- ? true
1367
- : selectedDataElements[`${purpose.uuid}-${element.uuid}`] ||
1368
- false,
1369
- })),
1370
- })),
1426
+ purposes: content.purposes.map((purpose) => {
1427
+ // Check if this is an already-consented purpose that was modified
1428
+ const isAlreadyConsented =
1429
+ categorizedPurposes?.alreadyConsented.some(
1430
+ (p) => p.uuid === purpose.uuid
1431
+ ) || false;
1432
+
1433
+ let purposeSelected = selectedPurposes[purpose.uuid] || false;
1434
+
1435
+ // If it's an already-consented purpose, check if any data elements were modified
1436
+ if (isAlreadyConsented) {
1437
+ const wasModified = purpose.data_elements.some((element) => {
1438
+ const combinedId = `${purpose.uuid}-${element.uuid}`;
1439
+ const currentSelection =
1440
+ selectedDataElements[combinedId] || false;
1441
+ const initialSelection =
1442
+ initialDataElementSelections[combinedId] || false;
1443
+ return currentSelection !== initialSelection;
1444
+ });
1445
+
1446
+ // If modified, treat as new selection (mark purpose as selected)
1447
+ if (wasModified) {
1448
+ purposeSelected = true;
1449
+ }
1450
+ }
1451
+
1452
+ return {
1453
+ ...purpose,
1454
+ selected: purposeSelected,
1455
+ data_elements: purpose.data_elements.map((element) => ({
1456
+ ...element,
1457
+ selected: element.required
1458
+ ? true
1459
+ : selectedDataElements[`${purpose.uuid}-${element.uuid}`] ||
1460
+ false,
1461
+ })),
1462
+ };
1463
+ }),
1371
1464
  declined: false,
1372
1465
  meta_data: applicationId
1373
1466
  ? { specific_uuid: applicationId }
@@ -1391,36 +1484,42 @@ export const RedactoNoticeConsent = ({
1391
1484
  }
1392
1485
  onError?.(error as Error);
1393
1486
  } finally {
1394
- setIsSubmitting(false);
1487
+ setIsSubmittingAccept(false);
1488
+ setIsSubmitting(false); // Keep for backward compatibility
1395
1489
  }
1396
1490
  };
1397
1491
 
1398
1492
  const handleDecline = useCallback(async () => {
1399
- // try {
1400
- // setIsSubmitting(true);
1401
- // if (content) {
1402
- // await submitConsentEvent(
1403
- // accessToken,
1404
- // content.notice_uuid,
1405
- // content.purposes.map((purpose) => ({
1406
- // ...purpose,
1407
- // selected: false,
1408
- // data_elements: purpose.data_elements.map((element) => ({
1409
- // ...element,
1410
- // selected: element.required ? true : false,
1411
- // })),
1412
- // })),
1413
- // true
1414
- // );
1415
- // }
1416
- // onDecline?.();
1417
- // } catch (error) {
1418
- // console.error("Error submitting consent:", error);
1419
- // onError?.(error as Error);
1420
- // } finally {
1421
- // setIsSubmitting(false);
1422
- // }
1423
- onDecline?.();
1493
+ try {
1494
+ setIsSubmittingDecline(true);
1495
+ setIsSubmitting(true); // Keep for backward compatibility
1496
+ // try {
1497
+ // if (content) {
1498
+ // await submitConsentEvent(
1499
+ // accessToken,
1500
+ // content.notice_uuid,
1501
+ // content.purposes.map((purpose) => ({
1502
+ // ...purpose,
1503
+ // selected: false,
1504
+ // data_elements: purpose.data_elements.map((element) => ({
1505
+ // ...element,
1506
+ // selected: element.required ? true : false,
1507
+ // })),
1508
+ // })),
1509
+ // true
1510
+ // );
1511
+ // }
1512
+ // } catch (error) {
1513
+ // console.error("Error submitting consent:", error);
1514
+ // onError?.(error as Error);
1515
+ // } finally {
1516
+ // setIsSubmitting(false);
1517
+ // }
1518
+ onDecline?.();
1519
+ } finally {
1520
+ setIsSubmittingDecline(false);
1521
+ setIsSubmitting(false); // Keep for backward compatibility
1522
+ }
1424
1523
  }, [onDecline]);
1425
1524
 
1426
1525
  // Guardian form handlers
@@ -2606,6 +2705,7 @@ export const RedactoNoticeConsent = ({
2606
2705
  getTranslatedText={getTranslatedText}
2607
2706
  isAlreadyConsented={true}
2608
2707
  selectedLanguage={selectedLanguage}
2708
+ initialDataElementSelections={initialDataElementSelections}
2609
2709
  />
2610
2710
  ))}
2611
2711
 
@@ -2671,13 +2771,13 @@ export const RedactoNoticeConsent = ({
2671
2771
  opacity: acceptDisabled ? 0.5 : 1,
2672
2772
  }}
2673
2773
  onClick={handleAccept}
2674
- disabled={acceptDisabled}
2774
+ disabled={acceptDisabled || isSubmittingDecline}
2675
2775
  aria-label={getTranslatedText(
2676
2776
  "confirm_button_text",
2677
2777
  content?.confirm_button_text || "Accept"
2678
2778
  )}
2679
2779
  >
2680
- {isSubmitting
2780
+ {isSubmittingAccept
2681
2781
  ? "Processing..."
2682
2782
  : getTranslatedText(
2683
2783
  "confirm_button_text",
@@ -2691,15 +2791,16 @@ export const RedactoNoticeConsent = ({
2691
2791
  ...responsiveStyles.button,
2692
2792
  ...styles.cancelButton,
2693
2793
  ...declineButtonStyle,
2794
+ opacity: isSubmittingAccept ? 0.5 : 1,
2694
2795
  }}
2695
2796
  onClick={handleDecline}
2696
- disabled={isSubmitting}
2797
+ disabled={isSubmittingDecline || isSubmittingAccept}
2697
2798
  aria-label={getTranslatedText(
2698
2799
  "decline_button_text",
2699
2800
  content?.decline_button_text || "Decline"
2700
2801
  )}
2701
2802
  >
2702
- {isSubmitting
2803
+ {isSubmittingDecline
2703
2804
  ? "Processing..."
2704
2805
  : getTranslatedText(
2705
2806
  "decline_button_text",