@salesforcedevs/dw-components 1.3.148-canary.2 → 1.3.148-canary.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/dw-components",
3
- "version": "1.3.148-canary.2",
3
+ "version": "1.3.148-canary.21",
4
4
  "description": "Lightning web components for https://developer.salesforce.com",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -40,7 +40,13 @@ dx-step-sequence-step:last-child dx-card-step::part(footer-container) {
40
40
 
41
41
  .body-markup {
42
42
  display: block;
43
- padding-bottom: var(--dx-g-spacing-md);
43
+ }
44
+
45
+ .final-email-address {
46
+ display: block;
47
+ margin-top: 20px;
48
+ line-height: 24px;
49
+ color: var(--dx-g-blue-vibrant-50);
44
50
  }
45
51
 
46
52
  .referral-email-field {
@@ -35,11 +35,15 @@
35
35
  <dw-j2s-form
36
36
  id={stepDatum.formData.formId}
37
37
  countries={countries}
38
- drm-recaptcha-site-key={drmRecaptchaSiteKey}
39
- form-metadata={stepDatum.formMetadata}
38
+ form-metadata={stepDatum.formData}
40
39
  incoming-user-info={formIncomingUserInfo}
41
40
  onformvaliditychange={handleFormValidityChange}
42
- ></dw-j2s-form>
41
+ recaptcha-token={recaptchaToken}
42
+ >
43
+ <div slot="form-captcha-slot">
44
+ <slot name="captcha-slot"></slot>
45
+ </div>
46
+ </dw-j2s-form>
43
47
  </template>
44
48
  </template>
45
49
  </div>
@@ -59,6 +63,7 @@
59
63
  loading={isLoading}
60
64
  onclick={primaryActionHandler}
61
65
  size="large"
66
+ target={primaryActionLinkTarget}
62
67
  variant={primaryActionVariant}
63
68
  >
64
69
  {stepDatum.primaryAction.text}
@@ -3,6 +3,7 @@ import StepSequence from "dx/stepSequence";
3
3
  import { getListWithUuids } from "dxUtils/list";
4
4
  import { track } from "dxUtils/analytics";
5
5
  import { toJson } from "dxUtils/normalizers";
6
+ import J2sForm from "dw/j2sForm";
6
7
 
7
8
  export type StepDatum = {
8
9
  bodyHtml?: string;
@@ -28,6 +29,7 @@ export type StepDatum = {
28
29
  iconSprite?: string;
29
30
  iconSymbol?: string;
30
31
  link?: string;
32
+ linkTarget?: string;
31
33
  text: string;
32
34
  variant?: string;
33
35
  };
@@ -36,22 +38,16 @@ export type StepDatum = {
36
38
  stepTitle: string;
37
39
  };
38
40
 
39
- function isNoAnimationCookieSet() {
40
- return document.cookie
41
- .split(";")
42
- .some((ck) => ck.trim() === "no_j2s_animation=true");
43
- }
44
-
45
41
  export default class J2sCardSequence extends LightningElement {
46
42
  _didRenderStaticInnerHtml = false;
47
43
  _isDrmLoading = false;
48
44
  _isFormSubmitting = false;
49
45
  _isJ2sFormInvalid = false;
50
46
  _isTbidLoading = false;
47
+ _recaptchaResetTimeoutId: number | null = null;
51
48
  _stepsData: StepDatum[] = [];
52
49
  @api animateTransitions = false;
53
50
  @api drmDomain: string = "https://drm.my.salesforce-sites.com";
54
- @api drmRecaptchaSiteKey: string = "";
55
51
  @api formHandler: string =
56
52
  "https://formhandlermule-1s9xrb.5sc6y6-4.usa-e2.cloudhub.io/form_handler";
57
53
  @api tbidApiBaseUrl: string | undefined;
@@ -63,34 +59,28 @@ export default class J2sCardSequence extends LightningElement {
63
59
  }
64
60
  set stepsData(value) {
65
61
  // normalize incoming value with uuid keys and non-required props (for cleaner HTML template), etc.
66
- this._stepsData = getListWithUuids<StepDatum>(toJson(value)).map(
67
- (stepDatum) => {
68
- const normalizedStepDatum = {
69
- ...stepDatum,
70
- headerData: stepDatum.headerData || {},
71
- primaryAction: {
72
- ...stepDatum.primaryAction,
73
- variant: stepDatum.primaryAction.variant || "primary",
74
- iconSprite:
75
- stepDatum.primaryAction.iconSprite || "general",
76
- iconSymbol:
77
- stepDatum.primaryAction.iconSymbol || "arrow-right"
78
- }
79
- };
80
-
81
- if (normalizedStepDatum.formData) {
82
- (normalizedStepDatum as any).formMetadata = JSON.stringify(
83
- normalizedStepDatum.formData
84
- );
62
+ const valueWithUuids = getListWithUuids<StepDatum>(toJson(value));
63
+ this._stepsData = valueWithUuids.map((stepDatum) => {
64
+ const normalizedStepDatum = {
65
+ ...stepDatum,
66
+ headerData: stepDatum.headerData || {},
67
+ primaryAction: {
68
+ ...stepDatum.primaryAction,
69
+ variant: stepDatum.primaryAction.variant || "primary",
70
+ iconSprite: stepDatum.primaryAction.iconSprite || "general",
71
+ iconSymbol:
72
+ stepDatum.primaryAction.iconSymbol || "arrow-right"
85
73
  }
74
+ };
86
75
 
87
- return normalizedStepDatum;
88
- }
89
- );
76
+ return normalizedStepDatum;
77
+ });
90
78
  }
91
79
 
92
80
  private countries = "[]";
93
81
  private currentSequenceStep = 0;
82
+ private didFormSubmissionFail = false;
83
+ private recaptchaToken = "";
94
84
  private sessionStorageId = "j2s-card-sequence";
95
85
  private tbidUserInfo: any; // TODO typings?
96
86
 
@@ -119,19 +109,32 @@ export default class J2sCardSequence extends LightningElement {
119
109
  .variant;
120
110
  }
121
111
 
112
+ private get primaryActionLinkTarget() {
113
+ return (
114
+ this.stepsData[this.currentSequenceStep || 0].primaryAction
115
+ .linkTarget || "_blank"
116
+ );
117
+ }
118
+
122
119
  private get formIncomingUserInfo() {
123
120
  if (!this.tbidUserInfo) {
124
- return "{}";
121
+ return {};
125
122
  }
126
123
 
127
- return JSON.stringify({
128
- firstName: this.tbidUserInfo.given_name,
129
- lastName: this.tbidUserInfo.family_name,
130
- email: this.tbidUserInfo.email,
131
- jobRole: this.tbidUserInfo.custom_attributes?.Role,
132
- companyName: this.tbidUserInfo.custom_attributes?.CompanyName,
133
- country: this.tbidUserInfo.custom_attributes?.Country
134
- });
124
+ return {
125
+ firstName: decodeURIComponent(this.tbidUserInfo.given_name),
126
+ lastName: decodeURIComponent(this.tbidUserInfo.family_name),
127
+ email: decodeURIComponent(this.tbidUserInfo.email),
128
+ jobRole: decodeURIComponent(
129
+ this.tbidUserInfo.custom_attributes?.Role
130
+ ),
131
+ companyName: decodeURIComponent(
132
+ this.tbidUserInfo.custom_attributes?.CompanyName
133
+ ),
134
+ country: decodeURIComponent(
135
+ this.tbidUserInfo.custom_attributes?.Country
136
+ )
137
+ };
135
138
  }
136
139
 
137
140
  private get initialStepIndex() {
@@ -158,20 +161,29 @@ export default class J2sCardSequence extends LightningElement {
158
161
  );
159
162
  }
160
163
 
164
+ @api
165
+ recaptchaCallback(responseToken: string) {
166
+ this.recaptchaToken = responseToken;
167
+ clearTimeout(this._recaptchaResetTimeoutId as number);
168
+ }
169
+
170
+ @api
171
+ recaptchaExpiredCallback() {
172
+ this.recaptchaToken = "";
173
+ clearTimeout(this._recaptchaResetTimeoutId as number);
174
+ }
175
+
161
176
  connectedCallback() {
162
177
  this.getCountryList();
163
178
  this.getTbidUserInfo();
164
179
  // This has to be defined here since it depends on a variable that depends on sessionStorage.
165
180
  this.currentSequenceStep = this.initialStepIndex;
166
181
  this.resetCustomButtonColors();
167
- if (isNoAnimationCookieSet()) {
168
- /* eslint-disable-next-line @lwc/lwc/no-api-reassignments */
169
- this.animateTransitions = false;
170
- }
171
182
  }
172
183
 
173
184
  renderedCallback() {
174
185
  if (this.isLoading) {
186
+ this._didRenderStaticInnerHtml = false; // only when `this.isLoading` is true will static HTML need to be re-rendered, since the content will have been replaced by the spinner
175
187
  return;
176
188
  }
177
189
 
@@ -179,6 +191,14 @@ export default class J2sCardSequence extends LightningElement {
179
191
  this.updateStaticInnerHtml();
180
192
  this._didRenderStaticInnerHtml = true;
181
193
  }
194
+
195
+ if (this.didFormSubmissionFail) {
196
+ const j2sFormEl = (this.template.querySelector(
197
+ "dw-j2s-form"
198
+ ) as unknown) as J2sForm | null;
199
+ j2sFormEl?.notifySubmitFailure();
200
+ this.didFormSubmissionFail = false;
201
+ }
182
202
  }
183
203
 
184
204
  handleFormValidityChange({ detail }: CustomEvent) {
@@ -188,6 +208,21 @@ export default class J2sCardSequence extends LightningElement {
188
208
  handleStepSequenceChange({ detail }: CustomEvent) {
189
209
  this.currentSequenceStep = detail.stepIndex;
190
210
  this.resetCustomButtonColors();
211
+
212
+ if (
213
+ this.currentSequenceStep !== 1 &&
214
+ this.recaptchaToken &&
215
+ this._recaptchaResetTimeoutId === null
216
+ ) {
217
+ // If the recaptcha element expires on its own while you are not on the 2nd (form) step, it messes with the
218
+ // page's layout (basically breaks it). So, we need to reset it _before_ expiration if you leave that step.
219
+ // We do it with a timeout so that the token is not immediately wiped if the user goes back and forth a bit.
220
+ this._recaptchaResetTimeoutId = window.setTimeout(() => {
221
+ this._recaptchaResetTimeoutId = null;
222
+ this.recaptchaToken = "";
223
+ (window as any).grecaptcha.reset();
224
+ }, 1000 * 30 /* 30 seconds */);
225
+ }
191
226
  }
192
227
 
193
228
  handleGetStarted(evt: Event) {
@@ -198,13 +233,12 @@ export default class J2sCardSequence extends LightningElement {
198
233
  evt.preventDefault();
199
234
  this.initialStepIndex = 1;
200
235
  this._isTbidLoading = true;
201
- // @ts-ignore: call out to the tbid-controller on DSC
202
- window.__startTbidLogin();
236
+ (window as any).__startTbidLogin?.(); // call out to DSC tbid controller
203
237
  }
204
238
  }
205
239
 
206
- handleFormSubmit(evt: Event) {
207
- if (!this.formHandler || this.isJ2sFormInvalid) {
240
+ async handleFormSubmit(evt: Event) {
241
+ if (!this.formHandler || this.isJ2sFormInvalid || !this.isLoggedIn) {
208
242
  return;
209
243
  }
210
244
 
@@ -212,8 +246,10 @@ export default class J2sCardSequence extends LightningElement {
212
246
  this._isFormSubmitting = true;
213
247
 
214
248
  try {
215
- const formData = (this.template.querySelector("dw-j2s-form") as any)
216
- ?.formDataForDrm;
249
+ const j2sFormEl = (this.template.querySelector(
250
+ "dw-j2s-form"
251
+ ) as unknown) as J2sForm | null;
252
+ const formData = j2sFormEl?.formDataForDrm;
217
253
  // For some countries, explicit approvals will be present; for others, it is implicitly assumed
218
254
  if (typeof formData.Opt_In__c !== "boolean") {
219
255
  formData.Opt_In__c = true;
@@ -221,10 +257,38 @@ export default class J2sCardSequence extends LightningElement {
221
257
  if (typeof formData.Privacy_Statement__c !== "boolean") {
222
258
  formData.Privacy_Statement__c = true;
223
259
  }
224
- console.log("would submit", JSON.stringify(formData, null, 2));
225
- this.jumpToStepIndex(2);
260
+
261
+ const res = await fetch(this.formHandler, {
262
+ method: "POST",
263
+ headers: {
264
+ "Captcha-Response": this.recaptchaToken
265
+ },
266
+ body: JSON.stringify({
267
+ ...formData,
268
+ handlerName: "J2S Registration Page",
269
+ TBID__c: this.tbidUserInfo.user_id
270
+ })
271
+ });
272
+
273
+ if (res.ok) {
274
+ const resData = await res.json();
275
+ const resOutput = resData.handlePostRequestOutput || resData;
276
+ if (resOutput?.status === "success") {
277
+ this.jumpToStepIndex(2, false);
278
+ this.initialStepIndex = 0; // don't save final step in storage; rather, reset the starting step
279
+ } else {
280
+ throw new Error(
281
+ `Form submission failed. (status: ${resOutput.status}, message: ${resOutput.errorMessage})`
282
+ );
283
+ }
284
+ } else {
285
+ throw new Error(
286
+ `Form submission failed. Non-ok response received from DRM.`
287
+ );
288
+ }
226
289
  } catch (ex) {
227
- console.log(ex); // TODO
290
+ this.didFormSubmissionFail = true;
291
+ console.error(ex);
228
292
  }
229
293
 
230
294
  this._isFormSubmitting = false;
@@ -281,14 +345,15 @@ export default class J2sCardSequence extends LightningElement {
281
345
  );
282
346
  }
283
347
 
284
- private jumpToStepIndex(stepIndex: number) {
285
- this.initialStepIndex = stepIndex; // update sessionStorage value
348
+ private jumpToStepIndex(stepIndex: number, andStoreIndex = true) {
349
+ if (andStoreIndex) {
350
+ this.initialStepIndex = stepIndex; // update sessionStorage value
351
+ }
286
352
  ((this.template.querySelector(
287
353
  "dx-step-sequence"
288
354
  ) as unknown) as StepSequence)?.jumpToStep(stepIndex);
289
355
  }
290
356
 
291
- // HTML is received via config from DSC and should be static after initial render.
292
357
  private updateStaticInnerHtml() {
293
358
  const bodyMarkupContainers = this.template.querySelectorAll(
294
359
  "span.body-markup"
@@ -304,7 +369,16 @@ export default class J2sCardSequence extends LightningElement {
304
369
  secondaryActionMarkupContainers[stepIndex];
305
370
 
306
371
  if (bodyMarkupContainer) {
307
- bodyMarkupContainer.innerHTML = bodyHtml || "";
372
+ let markup = bodyHtml;
373
+ if (stepIndex === 2) {
374
+ // User's email is supposed to show on final step
375
+ markup = `${bodyHtml}${
376
+ !this.formIncomingUserInfo.email
377
+ ? ""
378
+ : `<span class="final-email-address">${this.formIncomingUserInfo.email}</span>`
379
+ }`;
380
+ }
381
+ bodyMarkupContainer.innerHTML = markup || "";
308
382
  }
309
383
  if (secondaryActionMarkupContainer) {
310
384
  secondaryActionMarkupContainer.innerHTML =
@@ -316,7 +390,7 @@ export default class J2sCardSequence extends LightningElement {
316
390
  private async getCountryList() {
317
391
  this._isDrmLoading = true;
318
392
 
319
- const errorMsg = "Error while getting county list";
393
+ const errorMsg = "Error while getting country list";
320
394
  try {
321
395
  const res = await fetch(
322
396
  `${this.drmDomain}/utilities/services/apexrest/countries`
@@ -1,46 +1,61 @@
1
- export const stepsData = [
2
- {
3
- showStepNumber: "true",
4
- stepTitle: "Get started now.",
5
- bodyHtml:
6
- '<p class="dx-text-display-7">Register for Journey to Salesforce and you\'ll be on your way to becoming a Salesforce Developer.</p><ul class="dx-text-display-8"><li>Step 1: Log in or sign up for a free Trailblazer.me account</li><li>Step 2: Confirm your program registration</li><li>Step 3: Begin your journey!</li></ul>',
7
- primaryActionText: "Get Started"
8
- },
9
- {
10
- showStepNumber: "true",
11
- stepTitle: "Complete your registration.",
12
- formData: {
13
- countriesWithReferralInput: ["IN"],
14
- countriewWithRequiredJobRole: ["CA"],
15
- notAvailableAlertInfo: {
16
- title: "Not Available",
17
- body: 'The Journey to Salesforce program is not currently available in your country. See the <a href="https://www.google.com/">program FAQs</a> for a list of participating countries.'
1
+ export const stepSequenceData = {
2
+ animateTransitions: "true",
3
+ stepsData: [
4
+ {
5
+ bodyHtml:
6
+ '<p class="dx-text-display-7">Register for Journey to Salesforce and you\'ll be on your way to becoming a Salesforce Developer.</p><ul class="dx-text-display-8"><li>Step 1: Log in or sign up for a free Trailblazer.me account</li><li>Step 2: Confirm your program registration</li><li>Step 3: Begin your journey!</li></ul>',
7
+ primaryAction: {
8
+ text: "Get Started"
18
9
  },
19
- participatingCountryCodes: ["US", "CA", "IN"]
10
+ showStepNumber: "true",
11
+ stepTitle: "Get started now."
20
12
  },
21
- bodyHtml:
22
- '<p class="dx-text-display-7">You\'re almost done! Please review your information and agree to the Journey to Salesforce program terms and conditions.</p>',
23
- secondaryActionHtml:
24
- '<span class="required-notice">* Required to participate in Journey to Salesforce</span>',
25
- primaryActionText: "Complete Registration"
26
- },
27
- {
28
- showStepNumber: "true",
29
- stepTitle: "Your Registration is Confirmed",
30
- headerData: {
31
- iconSymbol: "check-circle",
32
- iconSprite: "general",
33
- iconColor: "green-vibrant-65",
34
- backgroundColor: "green-vibrant-95",
35
- textColor: "indigo-vibrant-20"
13
+ {
14
+ bodyHtml:
15
+ '<p class="dx-text-display-7">You\'re almost done! Please review your information and agree to the Journey to Salesforce program terms and conditions.</p>',
16
+ formData: {
17
+ countriesWithReferralInput: ["IN"],
18
+ countriesWithRequiredJobRole: ["CA"],
19
+ formId: "j2s-form",
20
+ j2sTerms:
21
+ 'I agree to the Journey to Salesforce Program Terms, subject to the <a href="http://www.google.com">Salesforce Program Agreement</a>.',
22
+ notAvailableAlertInfo: {
23
+ title: "Not Available",
24
+ body:
25
+ 'The Journey to Salesforce program is not currently available in your country. See the <a href="https://www.google.com/">program FAQs</a> for a list of participating countries.'
26
+ },
27
+ participatingCountryCodes: ["US", "CA", "IN"]
28
+ },
29
+ primaryAction: {
30
+ text: "Complete Registration"
31
+ },
32
+ secondaryActionHtml:
33
+ '<span class="required-notice">* Required to participate in Journey to Salesforce</span>',
34
+ showStepNumber: "true",
35
+ stepTitle: "Complete your registration."
36
36
  },
37
- bodyHtml:
38
- '<div class="dx-text-display-8"><p>Welcome to Journey to Salesforce! Congratulations on taking this step in your journey towards becoming a Salesforce Developer. Through this program you\'ll gain the knowledge, experience, and skills to help you excel in the Salesforce ecosystem.</p><p>To get started, click the button below to join the Journey to Salesforce community, and make sure to check your email for links to bookmark!</p>',
39
- secondaryActionHtml:
40
- '<span class="dx-text-display-7">Ready to jump in?</span>',
41
- primaryActionText: "Join Slack",
42
- primaryActionVariant: "custom",
43
- primaryActionIconSprite: "utility",
44
- primaryActionIconSymbol: "new_window"
45
- }
46
- ];
37
+ {
38
+ bodyHtml:
39
+ '<div class="dx-text-display-8"><p>Welcome to Journey to Salesforce! Congratulations on taking this step in your journey towards becoming a Salesforce Developer. Through this program you\'ll gain the knowledge, experience, and skills to help you excel in the Salesforce ecosystem.</p><p>To get started, click the button below to join the Journey to Salesforce community, and make sure to check your email for links to bookmark!</p></div>',
40
+ headerData: {
41
+ backgroundColor: "green-vibrant-95",
42
+ iconSymbol: "check-circle",
43
+ iconSprite: "general",
44
+ iconColor: "green-vibrant-65",
45
+ textColor: "indigo-vibrant-20"
46
+ },
47
+ primaryAction: {
48
+ iconSprite: "utility",
49
+ iconSymbol: "new_window",
50
+ link:
51
+ "https://salesforce-external.slack.com/archives/C05HJK4PGJV",
52
+ text: "Join Slack",
53
+ variant: "custom"
54
+ },
55
+ secondaryActionHtml:
56
+ '<span class="dx-text-display-7">Ready to jump in?</span>',
57
+ showStepNumber: "true",
58
+ stepTitle: "Your Registration is Confirmed"
59
+ }
60
+ ]
61
+ };
@@ -21,8 +21,8 @@
21
21
  }
22
22
 
23
23
  .form-row .terms {
24
- --dx-c-checkbox-text-line-height: 20px;
25
- --dx-c-checkbox-font-size: 14px;
24
+ --dx-c-checkbox-text-line-height: var(--dx-g-text-lg);
25
+ --dx-c-checkbox-font-size: var(--dx-g-text-sm);
26
26
 
27
27
  font-family: var(--dx-g-font-sans);
28
28
  font-size: var(--dx-c-checkbox-font-size);
@@ -44,3 +44,13 @@
44
44
  .form-terms-container {
45
45
  padding-top: var(--dx-g-spacing-sm);
46
46
  }
47
+
48
+ .form-captcha-slot {
49
+ align-items: center;
50
+ display: flex;
51
+ justify-content: center;
52
+ }
53
+
54
+ dx-input[disabled] {
55
+ --dx-c-input-background: revert;
56
+ }
@@ -10,7 +10,7 @@
10
10
  placeholder="Your first name"
11
11
  required
12
12
  validity-on-blur
13
- onblur={handleRequiredFieldUpdate}
13
+ onchange={handleFormFieldChange}
14
14
  value={state.firstName}
15
15
  ></dx-input>
16
16
  <dx-input
@@ -21,7 +21,7 @@
21
21
  placeholder="Your last name"
22
22
  required
23
23
  validity-on-blur
24
- onblur={handleRequiredFieldUpdate}
24
+ onchange={handleFormFieldChange}
25
25
  value={state.lastName}
26
26
  ></dx-input>
27
27
  </div>
@@ -34,21 +34,10 @@
34
34
  placeholder="Your company name"
35
35
  required
36
36
  validity-on-blur
37
- onblur={handleRequiredFieldUpdate}
38
37
  value={state.companyName}
38
+ onchange={handleFormFieldChange}
39
39
  ></dx-input>
40
40
  <dx-select
41
- if:false={isJobRoleRequired}
42
- class="job-role-field"
43
- label="Job Role"
44
- message-when-value-missing="Select your job role"
45
- name="DRM_Role__c"
46
- options={jobRoles}
47
- placeholder="Select Job Role"
48
- value={state.jobRole}
49
- ></dx-select>
50
- <dx-select
51
- if:true={isJobRoleRequired}
52
41
  class="job-role-field"
53
42
  label="Job Role"
54
43
  message-when-value-missing="Select your job role"
@@ -57,7 +46,7 @@
57
46
  placeholder="Select Job Role"
58
47
  required
59
48
  value={state.jobRole}
60
- onchange={handleRequiredFieldUpdate}
49
+ onchange={handleFormFieldChange}
61
50
  ></dx-select>
62
51
  </div>
63
52
  <div class="form-row">
@@ -68,8 +57,9 @@
68
57
  label="Email"
69
58
  missing-error-message="Enter your email address"
70
59
  validity-on-blur="true"
60
+ disabled
61
+ readonly="true"
71
62
  required
72
- onblur={handleRequiredFieldUpdate}
73
63
  value={state.email}
74
64
  ></dx-input>
75
65
  <dx-select
@@ -80,7 +70,7 @@
80
70
  placeholder="Select Country/Region"
81
71
  required
82
72
  value={state.country}
83
- onchange={handleCountryChange}
73
+ onchange={handleFormFieldChange}
84
74
  ></dx-select>
85
75
  </div>
86
76
  <div class="form-row" if:true={isCountryWithReferralInput}>
@@ -88,7 +78,9 @@
88
78
  name="Referral_Email__c"
89
79
  class="referral-email-field full-width"
90
80
  label="If anyone has referred you, please share their Email ID below, with their consent."
81
+ validity-on-blur="true"
91
82
  type="email"
83
+ onchange={handleFormFieldChange}
92
84
  ></dx-input>
93
85
  </div>
94
86
  <div class="form-row" if:true={isNotParticipatingCountry}>
@@ -99,6 +91,14 @@
99
91
  variant="accented"
100
92
  ></dx-alert>
101
93
  </div>
94
+ <div class="form-row" if:true={showSubmitFailureAlert}>
95
+ <dx-alert
96
+ alert-info={formSubmitFailureAlertInfo}
97
+ class="full-width"
98
+ type="danger"
99
+ variant="accented"
100
+ ></dx-alert>
101
+ </div>
102
102
  <div class="form-terms-container">
103
103
  <div class="form-row" if:true={state.showMarketingText}>
104
104
  <dx-checkbox
@@ -106,7 +106,7 @@
106
106
  name="Opt_In__c"
107
107
  required
108
108
  class="terms"
109
- onchange={handleRequiredFieldUpdate}
109
+ onchange={handleFormFieldChange}
110
110
  disabled={isNotParticipatingCountry}
111
111
  >
112
112
  <span lwc:dom="manual" class="marketing-terms"></span>
@@ -118,7 +118,7 @@
118
118
  name="j2s_terms"
119
119
  required
120
120
  class="terms"
121
- onchange={handleRequiredFieldUpdate}
121
+ onchange={handleFormFieldChange}
122
122
  disabled={isNotParticipatingCountry}
123
123
  >
124
124
  <span lwc:dom="manual" class="j2s-terms"></span>
@@ -131,7 +131,7 @@
131
131
  name="Privacy_Statement__c"
132
132
  required
133
133
  class="terms"
134
- onchange={handleRequiredFieldUpdate}
134
+ onchange={handleFormFieldChange}
135
135
  disabled={isNotParticipatingCountry}
136
136
  >
137
137
  <span lwc:dom="manual" class="legal-terms"></span>
@@ -154,7 +154,7 @@
154
154
  class="legal-terms no-checkbox"
155
155
  ></span>
156
156
  </div>
157
- <div if:true={recaptcha} class="captcha" lwc:dom="manual"></div>
157
+ <slot class="form-captcha-slot" name="form-captcha-slot"></slot>
158
158
  </div>
159
159
  </form>
160
160
  </template>
@@ -1,5 +1,6 @@
1
1
  import { toJson } from "dxUtils/normalizers";
2
2
  import { api } from "lwc";
3
+ import debounce from "debounce";
3
4
  import { LightningElementWithState } from "dxBaseElements/lightningElementWithState";
4
5
 
5
6
  // Hard-coding for now because we can't get information about an endpoint for this
@@ -33,15 +34,13 @@ export type J2sFormState = {
33
34
  export default class J2sForm extends LightningElementWithState<J2sFormState> {
34
35
  private _countriesJson = "";
35
36
  private _countriesLookup: any = {}; // TODO typings
36
- private _didRenderRecaptcha = false;
37
37
  private _didSetFormId = false;
38
- private _formMetadata: any = {}; // TODO typings
39
- private _incomingUserInfo: any = {}; // TODO typings
40
38
  private _isInitialRender = true;
39
+ private _recaptchaToken = "";
41
40
 
42
- @api drmRecaptchaSiteKey = "";
41
+ @api formMetadata: any = {};
43
42
  @api id = "";
44
- private recaptcha: any = null;
43
+ @api incomingUserInfo: any = {};
45
44
  @api referralText = "";
46
45
 
47
46
  @api
@@ -60,42 +59,41 @@ export default class J2sForm extends LightningElementWithState<J2sFormState> {
60
59
  this._countriesLookup = countriesLookup;
61
60
  }
62
61
 
63
- @api
64
- get incomingUserInfo() {
65
- return this._incomingUserInfo;
66
- }
67
- set incomingUserInfo(value: string) {
68
- this._incomingUserInfo = toJson(value);
69
- }
70
-
71
- @api
72
- get formMetadata() {
73
- return this._formMetadata;
74
- }
75
- set formMetadata(value: string) {
76
- this._formMetadata = toJson(value);
77
- }
78
-
79
62
  @api
80
63
  get formDataForDrm() {
81
64
  // We should be able to just get the formData from the form automatically using `new FormData(theForm)`,
82
65
  // but our custom form elements don't seem to implement what it is required for that currently. See:
83
66
  // https://web.dev/more-capable-form-controls/#defining-a-form-associated-custom-element
84
67
  const formData: any = {};
85
- this.template.querySelectorAll("[name]").forEach((namedElement) => {
86
- const typedElement = namedElement as HTMLInputElement;
87
- formData[typedElement.name] =
88
- typeof typedElement.checked === "boolean"
89
- ? typedElement.checked
90
- : typedElement.value;
91
- });
68
+ this.template
69
+ .querySelectorAll("form [name]")
70
+ .forEach((namedElement) => {
71
+ const typedElement = namedElement as HTMLInputElement;
72
+ formData[typedElement.name] =
73
+ typeof typedElement.checked === "boolean"
74
+ ? typedElement.checked
75
+ : typedElement.value;
76
+ });
92
77
  return formData;
93
78
  }
94
79
 
80
+ @api
81
+ get recaptchaToken() {
82
+ return this._recaptchaToken;
83
+ }
84
+ set recaptchaToken(value: string) {
85
+ this._recaptchaToken = value;
86
+ if (!this._isInitialRender) {
87
+ // Treat recaptcha checkbox as a required form field even though not technically marked as such:
88
+ this.handleFormFieldChange({ target: { required: true } } as any);
89
+ }
90
+ }
91
+
95
92
  private participatingCountryCodes = ["US", "CA", "IN"];
96
93
  private countriesWithReferralInput = ["IN"];
97
94
  private countriesWithRequiredJobRole = ["CA"];
98
95
  private requiredFormFields: HTMLElement[] = [];
96
+ private showSubmitFailureAlert = false;
99
97
 
100
98
  get isNotParticipatingCountry() {
101
99
  return !this.isParticipatingCountry(this.state.country);
@@ -105,12 +103,12 @@ export default class J2sForm extends LightningElementWithState<J2sFormState> {
105
103
  return this.countriesWithReferralInput.includes(this.state.country);
106
104
  }
107
105
 
108
- get isJobRoleRequired() {
109
- return this.countriesWithRequiredJobRole.includes(this.state.country);
106
+ get notAvailableAlertInfo() {
107
+ return this.formMetadata.notAvailableAlertInfo;
110
108
  }
111
109
 
112
- get notAvailableAlertInfo() {
113
- return (this.formMetadata as any).notAvailableAlertInfo;
110
+ get formSubmitFailureAlertInfo() {
111
+ return this.formMetadata.formSubmitFailureAlertInfo;
114
112
  }
115
113
 
116
114
  get jobRoles() {
@@ -122,7 +120,7 @@ export default class J2sForm extends LightningElementWithState<J2sFormState> {
122
120
 
123
121
  connectedCallback() {
124
122
  const initialState: Partial<J2sFormState> = {
125
- ...this._incomingUserInfo,
123
+ ...this.incomingUserInfo,
126
124
  isFormValid: false,
127
125
  showMarketingText: true
128
126
  };
@@ -132,26 +130,20 @@ export default class J2sForm extends LightningElementWithState<J2sFormState> {
132
130
  if (initialCountryData) {
133
131
  initialState.showLegalCheckbox =
134
132
  initialCountryData.showLegalCheckbox;
133
+ initialState.showMarketingText =
134
+ initialCountryData.showMarketingText;
135
135
  initialState.legalText = initialCountryData.legalText;
136
136
  initialState.marketingText = initialCountryData.marketingText;
137
137
  initialState.learnMore = initialCountryData.learnMore;
138
138
  }
139
139
 
140
140
  this.state = initialState as J2sFormState;
141
-
142
- if ((window as any).grecaptcha) {
143
- this.setRecaptcha();
144
- } else {
145
- window.addEventListener("recaptchaloaded", this.setRecaptcha);
146
- }
147
141
  }
148
142
 
149
143
  renderedCallback(): void {
150
144
  if (this._isInitialRender) {
151
- this.updateInnerHtml(
152
- "span.j2s-terms",
153
- (this.formMetadata as any).j2sTerms
154
- );
145
+ this.updateInnerHtml("span.j2s-terms", this.formMetadata.j2sTerms);
146
+ this._isInitialRender = false;
155
147
  }
156
148
 
157
149
  if (
@@ -197,32 +189,49 @@ export default class J2sForm extends LightningElementWithState<J2sFormState> {
197
189
  this._didSetFormId = true;
198
190
  this.template.querySelector("form")?.setAttribute("id", this.id);
199
191
  }
200
-
201
- if (!this._didRenderRecaptcha && this.recaptcha) {
202
- this._didRenderRecaptcha = true;
203
- this.recaptcha.render(this.template.querySelector(".captcha"), {
204
- sitekey: this.drmRecaptchaSiteKey,
205
- callback: (recaptchaToken: string) => {
206
- console.log("got captcha", recaptchaToken);
207
- },
208
- "expired-callback": () => {},
209
- "error-callback": (...args: any[]) => {
210
- console.log("RECAPTCHA ERROR");
211
- console.log(...args);
212
- }
213
- });
214
- }
215
192
  }
216
193
 
217
- disconnectedCallback() {
218
- window.removeEventListener("recaptchaloaded", this.setRecaptcha);
194
+ handleLearnMore() {
195
+ this.template
196
+ .querySelector(".legal-terms-learn-more")
197
+ ?.classList.toggle("hidden");
219
198
  }
220
199
 
221
- setRecaptcha = () => {
222
- this.recaptcha = (window as any).grecaptcha;
200
+ handleFormFieldChange = (evt: CustomEvent) => {
201
+ const callbacks: Function[] = [];
202
+
203
+ // Event target information must be read prior to debouncing; otherwise, the target is gone after the debounce.
204
+ if ((evt.target as HTMLInputElement)?.name === "MailingCountry") {
205
+ const { value } = evt.detail;
206
+ callbacks.push(() => this.handleCountryChange(value));
207
+ } else if ((evt.target as HTMLInputElement)?.required) {
208
+ callbacks.push(() => this.handleRequiredFieldUpdate());
209
+ }
210
+
211
+ callbacks.push(() => {
212
+ // Remove submit failure message once any change is made to the form.
213
+ if (this.showSubmitFailureAlert) {
214
+ this.showSubmitFailureAlert = false;
215
+ }
216
+ });
217
+
218
+ // Don't do unnecessary work too often if form fields are rapidly being changed
219
+ this.debouncedFormFieldChangeHandler(callbacks);
223
220
  };
224
221
 
225
- handleRequiredFieldUpdate() {
222
+ @api
223
+ notifySubmitFailure() {
224
+ this.showSubmitFailureAlert = true;
225
+ }
226
+
227
+ private debouncedFormFieldChangeHandler = debounce(
228
+ (callbacks: Function[]) => {
229
+ callbacks.forEach((callback) => callback());
230
+ },
231
+ 1000 / 15 /* 15fps seems reasonable */
232
+ );
233
+
234
+ private handleRequiredFieldUpdate() {
226
235
  const isFormNowValid = this.checkValidity();
227
236
  if (this.state.isFormValid !== isFormNowValid) {
228
237
  this.setState({
@@ -231,16 +240,16 @@ export default class J2sForm extends LightningElementWithState<J2sFormState> {
231
240
  }
232
241
  }
233
242
 
234
- handleCountryChange({ detail }: CustomEvent) {
235
- const { value } = detail;
236
- const countryData = this._countriesLookup[value];
243
+ private handleCountryChange(countryCode: string) {
244
+ const countryData = this._countriesLookup[countryCode];
237
245
 
238
246
  this.setState({
239
- country: value,
247
+ country: countryCode,
240
248
  learnMore: countryData.learnMore,
241
249
  legalText: countryData.legalText,
242
250
  marketingText: countryData.marketingText,
243
- showLegalCheckbox: countryData.showLegalCheckbox
251
+ showLegalCheckbox: countryData.showLegalCheckbox,
252
+ showMarketingText: countryData.showMarketingText
244
253
  });
245
254
 
246
255
  // Because updating the country can update the required fields, we need to re-set form
@@ -252,12 +261,6 @@ export default class J2sForm extends LightningElementWithState<J2sFormState> {
252
261
  );
253
262
  }
254
263
 
255
- handleLearnMore() {
256
- this.template
257
- .querySelector(".legal-terms-learn-more")
258
- ?.classList.toggle("hidden");
259
- }
260
-
261
264
  private updateInnerHtml(selector: string, html: string): void {
262
265
  const element = this.template.querySelector(selector);
263
266
  if (element) {
@@ -271,6 +274,7 @@ export default class J2sForm extends LightningElementWithState<J2sFormState> {
271
274
 
272
275
  private checkValidity() {
273
276
  return (
277
+ // Boolean(this.recaptchaToken) &&
274
278
  this.isParticipatingCountry(this.state.country) &&
275
279
  this.requiredFormFields.every(
276
280
  (requiredFormField) =>