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

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.0",
3
+ "version": "1.3.148-canary.2",
4
4
  "description": "Lightning web components for https://developer.salesforce.com",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -35,6 +35,7 @@
35
35
  <dw-j2s-form
36
36
  id={stepDatum.formData.formId}
37
37
  countries={countries}
38
+ drm-recaptcha-site-key={drmRecaptchaSiteKey}
38
39
  form-metadata={stepDatum.formMetadata}
39
40
  incoming-user-info={formIncomingUserInfo}
40
41
  onformvaliditychange={handleFormValidityChange}
@@ -52,6 +53,7 @@
52
53
  <dx-button
53
54
  class="primary-action"
54
55
  disabled={isJ2sFormInvalid}
56
+ href={stepDatum.primaryAction.link}
55
57
  icon-sprite={stepDatum.primaryAction.iconSprite}
56
58
  icon-symbol={stepDatum.primaryAction.iconSymbol}
57
59
  loading={isLoading}
@@ -1,8 +1,8 @@
1
1
  import { api, LightningElement } from "lwc";
2
2
  import StepSequence from "dx/stepSequence";
3
3
  import { getListWithUuids } from "dxUtils/list";
4
+ import { track } from "dxUtils/analytics";
4
5
  import { toJson } from "dxUtils/normalizers";
5
- import Button from "dx/button";
6
6
 
7
7
  export type StepDatum = {
8
8
  bodyHtml?: string;
@@ -25,23 +25,35 @@ export type StepDatum = {
25
25
  textColor?: string;
26
26
  };
27
27
  primaryAction: {
28
- text: string;
29
- variant?: string;
30
28
  iconSprite?: string;
31
29
  iconSymbol?: string;
30
+ link?: string;
31
+ text: string;
32
+ variant?: string;
32
33
  };
33
34
  secondaryActionHtml?: string;
34
35
  showStepNumber?: boolean;
35
36
  stepTitle: string;
36
37
  };
37
38
 
39
+ function isNoAnimationCookieSet() {
40
+ return document.cookie
41
+ .split(";")
42
+ .some((ck) => ck.trim() === "no_j2s_animation=true");
43
+ }
44
+
38
45
  export default class J2sCardSequence extends LightningElement {
46
+ _didRenderStaticInnerHtml = false;
39
47
  _isDrmLoading = false;
48
+ _isFormSubmitting = false;
40
49
  _isJ2sFormInvalid = false;
41
50
  _isTbidLoading = false;
42
51
  _stepsData: StepDatum[] = [];
43
52
  @api animateTransitions = false;
44
53
  @api drmDomain: string = "https://drm.my.salesforce-sites.com";
54
+ @api drmRecaptchaSiteKey: string = "";
55
+ @api formHandler: string =
56
+ "https://formhandlermule-1s9xrb.5sc6y6-4.usa-e2.cloudhub.io/form_handler";
45
57
  @api tbidApiBaseUrl: string | undefined;
46
58
  @api useHistory: boolean = false;
47
59
 
@@ -50,9 +62,9 @@ export default class J2sCardSequence extends LightningElement {
50
62
  return this._stepsData;
51
63
  }
52
64
  set stepsData(value) {
65
+ // normalize incoming value with uuid keys and non-required props (for cleaner HTML template), etc.
53
66
  this._stepsData = getListWithUuids<StepDatum>(toJson(value)).map(
54
67
  (stepDatum) => {
55
- // small bit of normalization for cleaner HTML templates with non-required props, etc.
56
68
  const normalizedStepDatum = {
57
69
  ...stepDatum,
58
70
  headerData: stepDatum.headerData || {},
@@ -83,7 +95,9 @@ export default class J2sCardSequence extends LightningElement {
83
95
  private tbidUserInfo: any; // TODO typings?
84
96
 
85
97
  private get isLoading() {
86
- return this._isDrmLoading || this._isTbidLoading;
98
+ return (
99
+ this._isDrmLoading || this._isTbidLoading || this._isFormSubmitting
100
+ );
87
101
  }
88
102
 
89
103
  private get isLoggedIn() {
@@ -150,6 +164,10 @@ export default class J2sCardSequence extends LightningElement {
150
164
  // This has to be defined here since it depends on a variable that depends on sessionStorage.
151
165
  this.currentSequenceStep = this.initialStepIndex;
152
166
  this.resetCustomButtonColors();
167
+ if (isNoAnimationCookieSet()) {
168
+ /* eslint-disable-next-line @lwc/lwc/no-api-reassignments */
169
+ this.animateTransitions = false;
170
+ }
153
171
  }
154
172
 
155
173
  renderedCallback() {
@@ -157,24 +175,21 @@ export default class J2sCardSequence extends LightningElement {
157
175
  return;
158
176
  }
159
177
 
160
- const { bodyHtml, secondaryActionHtml } =
161
- this.stepsData[this.currentSequenceStep];
162
- const bodyMarkupContainer =
163
- this.template.querySelectorAll("span.body-markup")[
164
- this.currentSequenceStep
165
- ];
166
- const secondaryActionMarkupContainer = this.template.querySelectorAll(
167
- "span.secondary-action-markup"
168
- )[this.currentSequenceStep];
169
- if (bodyMarkupContainer) {
170
- bodyMarkupContainer.innerHTML = bodyHtml || "";
171
- }
172
- if (secondaryActionMarkupContainer) {
173
- secondaryActionMarkupContainer.innerHTML =
174
- secondaryActionHtml || "";
178
+ if (!this._didRenderStaticInnerHtml) {
179
+ this.updateStaticInnerHtml();
180
+ this._didRenderStaticInnerHtml = true;
175
181
  }
176
182
  }
177
183
 
184
+ handleFormValidityChange({ detail }: CustomEvent) {
185
+ this.isJ2sFormInvalid = !detail.isValid;
186
+ }
187
+
188
+ handleStepSequenceChange({ detail }: CustomEvent) {
189
+ this.currentSequenceStep = detail.stepIndex;
190
+ this.resetCustomButtonColors();
191
+ }
192
+
178
193
  handleGetStarted(evt: Event) {
179
194
  // If the user is not yet logged in, we need to prevent moving immediately to the next
180
195
  // step, but set storage up so that they are on the second step when they return to the
@@ -182,32 +197,61 @@ export default class J2sCardSequence extends LightningElement {
182
197
  if (!this.isLoggedIn) {
183
198
  evt.preventDefault();
184
199
  this.initialStepIndex = 1;
200
+ this._isTbidLoading = true;
185
201
  // @ts-ignore: call out to the tbid-controller on DSC
186
202
  window.__startTbidLogin();
187
203
  }
188
204
  }
189
205
 
190
- handleFormValidityChange({ detail }: CustomEvent) {
191
- this.isJ2sFormInvalid = !detail.isValid;
192
- }
206
+ handleFormSubmit(evt: Event) {
207
+ if (!this.formHandler || this.isJ2sFormInvalid) {
208
+ return;
209
+ }
193
210
 
194
- handleStepSequenceChange({ detail }: CustomEvent) {
195
- this.currentSequenceStep = detail.stepIndex;
196
- this.resetCustomButtonColors();
211
+ evt.preventDefault(); // don't proceed automatically to next step
212
+ this._isFormSubmitting = true;
213
+
214
+ try {
215
+ const formData = (this.template.querySelector("dw-j2s-form") as any)
216
+ ?.formDataForDrm;
217
+ // For some countries, explicit approvals will be present; for others, it is implicitly assumed
218
+ if (typeof formData.Opt_In__c !== "boolean") {
219
+ formData.Opt_In__c = true;
220
+ }
221
+ if (typeof formData.Privacy_Statement__c !== "boolean") {
222
+ formData.Privacy_Statement__c = true;
223
+ }
224
+ console.log("would submit", JSON.stringify(formData, null, 2));
225
+ this.jumpToStepIndex(2);
226
+ } catch (ex) {
227
+ console.log(ex); // TODO
228
+ }
229
+
230
+ this._isFormSubmitting = false;
197
231
  }
198
232
 
199
233
  primaryActionHandler(evt: Event) {
200
- if ((evt.target as Button)?.disabled) {
234
+ const target = evt.target as HTMLButtonElement;
235
+
236
+ if (!target || target.disabled) {
201
237
  evt.preventDefault();
202
238
  return;
203
239
  }
204
240
 
241
+ track(target, "custEv_ctaButtonClick", {
242
+ click_text: target.textContent,
243
+ element_title: this.stepsData[this.currentSequenceStep].stepTitle,
244
+ click_url:
245
+ this.stepsData[this.currentSequenceStep].primaryAction.link ||
246
+ "",
247
+ element_type: "button",
248
+ content_category: "cta"
249
+ });
250
+
205
251
  if (!this.currentSequenceStep) {
206
252
  this.handleGetStarted(evt);
207
- } else if (this.currentSequenceStep === 2) {
208
- // TODO: send data to DRM
209
- } else if (this.currentSequenceStep === 3) {
210
- // TODO: Join Slack
253
+ } else if (this.currentSequenceStep === 1) {
254
+ this.handleFormSubmit(evt);
211
255
  }
212
256
  }
213
257
 
@@ -237,6 +281,38 @@ export default class J2sCardSequence extends LightningElement {
237
281
  );
238
282
  }
239
283
 
284
+ private jumpToStepIndex(stepIndex: number) {
285
+ this.initialStepIndex = stepIndex; // update sessionStorage value
286
+ ((this.template.querySelector(
287
+ "dx-step-sequence"
288
+ ) as unknown) as StepSequence)?.jumpToStep(stepIndex);
289
+ }
290
+
291
+ // HTML is received via config from DSC and should be static after initial render.
292
+ private updateStaticInnerHtml() {
293
+ const bodyMarkupContainers = this.template.querySelectorAll(
294
+ "span.body-markup"
295
+ );
296
+ const secondaryActionMarkupContainers = this.template.querySelectorAll(
297
+ "span.secondary-action-markup"
298
+ );
299
+
300
+ this.stepsData.forEach((step, stepIndex) => {
301
+ const { bodyHtml, secondaryActionHtml } = step;
302
+ const bodyMarkupContainer = bodyMarkupContainers[stepIndex];
303
+ const secondaryActionMarkupContainer =
304
+ secondaryActionMarkupContainers[stepIndex];
305
+
306
+ if (bodyMarkupContainer) {
307
+ bodyMarkupContainer.innerHTML = bodyHtml || "";
308
+ }
309
+ if (secondaryActionMarkupContainer) {
310
+ secondaryActionMarkupContainer.innerHTML =
311
+ secondaryActionHtml || "";
312
+ }
313
+ });
314
+ }
315
+
240
316
  private async getCountryList() {
241
317
  this._isDrmLoading = true;
242
318
 
@@ -252,6 +328,11 @@ export default class J2sCardSequence extends LightningElement {
252
328
  this.countries = JSON.stringify(data);
253
329
  } catch (error) {
254
330
  console.error(error);
331
+ if (this.initialStepIndex > 0) {
332
+ // Page loaded on a step higher than the first, but we can't get data for the form;
333
+ // send user back to the start for now.
334
+ this.jumpToStepIndex(0);
335
+ }
255
336
  }
256
337
 
257
338
  this._isDrmLoading = false;
@@ -272,12 +353,7 @@ export default class J2sCardSequence extends LightningElement {
272
353
  if (!this.tbidUserInfo && this.initialStepIndex > 0) {
273
354
  // login failed and the user was previously beyond the start step; need to send them
274
355
  // back to the start:
275
- this.initialStepIndex = 0;
276
- (
277
- this.template.querySelector(
278
- "dx-step-sequence"
279
- ) as unknown as StepSequence
280
- )?.jumpToStep(0);
356
+ this.jumpToStepIndex(0);
281
357
  }
282
358
 
283
359
  this._isTbidLoading = false;
@@ -1,11 +1,12 @@
1
1
  <template>
2
+ <!-- For the names of the form fields, see: https://salesforce.quip.com/daInAFETbPGm -->
2
3
  <form class="j2s-form">
3
4
  <div class="form-row">
4
5
  <dx-input
5
6
  class="first-name-field"
6
7
  label="First Name"
7
8
  missing-error-message="Enter your first name"
8
- name="first_name"
9
+ name="FirstName"
9
10
  placeholder="Your first name"
10
11
  required
11
12
  validity-on-blur
@@ -16,7 +17,7 @@
16
17
  class="last-name-field"
17
18
  label="Last Name"
18
19
  missing-error-message="Enter your last name"
19
- name="last_name"
20
+ name="LastName"
20
21
  placeholder="Your last name"
21
22
  required
22
23
  validity-on-blur
@@ -29,7 +30,7 @@
29
30
  class="company-name-field"
30
31
  label="Company Name"
31
32
  missing-error-message="Enter your company name"
32
- name="company_name"
33
+ name="Company__c"
33
34
  placeholder="Your company name"
34
35
  required
35
36
  validity-on-blur
@@ -41,7 +42,7 @@
41
42
  class="job-role-field"
42
43
  label="Job Role"
43
44
  message-when-value-missing="Select your job role"
44
- name="job_role"
45
+ name="DRM_Role__c"
45
46
  options={jobRoles}
46
47
  placeholder="Select Job Role"
47
48
  value={state.jobRole}
@@ -51,7 +52,7 @@
51
52
  class="job-role-field"
52
53
  label="Job Role"
53
54
  message-when-value-missing="Select your job role"
54
- name="job_role"
55
+ name="DRM_Role__c"
55
56
  options={jobRoles}
56
57
  placeholder="Select Job Role"
57
58
  required
@@ -61,7 +62,7 @@
61
62
  </div>
62
63
  <div class="form-row">
63
64
  <dx-input
64
- name="email"
65
+ name="Email"
65
66
  type="email"
66
67
  placeholder="Your email address"
67
68
  label="Email"
@@ -74,7 +75,7 @@
74
75
  <dx-select
75
76
  label="Country/Region"
76
77
  message-when-value-missing="Select your country/region"
77
- name="country"
78
+ name="MailingCountry"
78
79
  options={countries}
79
80
  placeholder="Select Country/Region"
80
81
  required
@@ -84,7 +85,7 @@
84
85
  </div>
85
86
  <div class="form-row" if:true={isCountryWithReferralInput}>
86
87
  <dx-input
87
- name="referral_email"
88
+ name="Referral_Email__c"
88
89
  class="referral-email-field full-width"
89
90
  label="If anyone has referred you, please share their Email ID below, with their consent."
90
91
  type="email"
@@ -102,7 +103,7 @@
102
103
  <div class="form-row" if:true={state.showMarketingText}>
103
104
  <dx-checkbox
104
105
  value="marketing-terms"
105
- name="marketing_terms"
106
+ name="Opt_In__c"
106
107
  required
107
108
  class="terms"
108
109
  onchange={handleRequiredFieldUpdate}
@@ -127,7 +128,7 @@
127
128
  <dx-checkbox
128
129
  if:true={state.showLegalCheckbox}
129
130
  value="legal-terms"
130
- name="legal_terms"
131
+ name="Privacy_Statement__c"
131
132
  required
132
133
  class="terms"
133
134
  onchange={handleRequiredFieldUpdate}
@@ -153,6 +154,7 @@
153
154
  class="legal-terms no-checkbox"
154
155
  ></span>
155
156
  </div>
157
+ <div if:true={recaptcha} class="captcha" lwc:dom="manual"></div>
156
158
  </div>
157
159
  </form>
158
160
  </template>
@@ -31,13 +31,17 @@ export type J2sFormState = {
31
31
  };
32
32
 
33
33
  export default class J2sForm extends LightningElementWithState<J2sFormState> {
34
- private _didSetFormId = false;
35
- private _isInitialRender = true;
36
- private _incomingUserInfo: any = {}; // TODO typings
37
- private _formMetadata: any = {}; // TODO typings
38
34
  private _countriesJson = "";
39
35
  private _countriesLookup: any = {}; // TODO typings
36
+ private _didRenderRecaptcha = false;
37
+ private _didSetFormId = false;
38
+ private _formMetadata: any = {}; // TODO typings
39
+ private _incomingUserInfo: any = {}; // TODO typings
40
+ private _isInitialRender = true;
41
+
42
+ @api drmRecaptchaSiteKey = "";
40
43
  @api id = "";
44
+ private recaptcha: any = null;
41
45
  @api referralText = "";
42
46
 
43
47
  @api
@@ -72,6 +76,22 @@ export default class J2sForm extends LightningElementWithState<J2sFormState> {
72
76
  this._formMetadata = toJson(value);
73
77
  }
74
78
 
79
+ @api
80
+ get formDataForDrm() {
81
+ // We should be able to just get the formData from the form automatically using `new FormData(theForm)`,
82
+ // but our custom form elements don't seem to implement what it is required for that currently. See:
83
+ // https://web.dev/more-capable-form-controls/#defining-a-form-associated-custom-element
84
+ 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
+ });
92
+ return formData;
93
+ }
94
+
75
95
  private participatingCountryCodes = ["US", "CA", "IN"];
76
96
  private countriesWithReferralInput = ["IN"];
77
97
  private countriesWithRequiredJobRole = ["CA"];
@@ -118,6 +138,12 @@ export default class J2sForm extends LightningElementWithState<J2sFormState> {
118
138
  }
119
139
 
120
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
+ }
121
147
  }
122
148
 
123
149
  renderedCallback(): void {
@@ -141,7 +167,7 @@ export default class J2sForm extends LightningElementWithState<J2sFormState> {
141
167
  if (this.state.learnMore) {
142
168
  this.updateInnerHtml(
143
169
  "span.legal-terms-learn-more",
144
- this.state.learnMore
170
+ `&nbsp;${this.state.learnMore}`
145
171
  );
146
172
  } else {
147
173
  this.updateInnerHtml("span.legal-terms-learn-more", "");
@@ -171,21 +197,33 @@ export default class J2sForm extends LightningElementWithState<J2sFormState> {
171
197
  this._didSetFormId = true;
172
198
  this.template.querySelector("form")?.setAttribute("id", this.id);
173
199
  }
174
- }
175
200
 
176
- private updateInnerHtml(selector: string, html: string): void {
177
- const element = this.template.querySelector(selector);
178
- if (element) {
179
- element.innerHTML = html;
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
+ });
180
214
  }
181
215
  }
182
216
 
183
- private isParticipatingCountry(country: string) {
184
- return this.participatingCountryCodes.includes(country);
217
+ disconnectedCallback() {
218
+ window.removeEventListener("recaptchaloaded", this.setRecaptcha);
185
219
  }
186
220
 
221
+ setRecaptcha = () => {
222
+ this.recaptcha = (window as any).grecaptcha;
223
+ };
224
+
187
225
  handleRequiredFieldUpdate() {
188
- const isFormNowValid = this.checkValidity(this.state.country);
226
+ const isFormNowValid = this.checkValidity();
189
227
  if (this.state.isFormValid !== isFormNowValid) {
190
228
  this.setState({
191
229
  isFormValid: isFormNowValid
@@ -196,14 +234,22 @@ export default class J2sForm extends LightningElementWithState<J2sFormState> {
196
234
  handleCountryChange({ detail }: CustomEvent) {
197
235
  const { value } = detail;
198
236
  const countryData = this._countriesLookup[value];
237
+
199
238
  this.setState({
200
239
  country: value,
201
- isFormValid: this.checkValidity(value),
202
240
  learnMore: countryData.learnMore,
203
241
  legalText: countryData.legalText,
204
242
  marketingText: countryData.marketingText,
205
243
  showLegalCheckbox: countryData.showLegalCheckbox
206
244
  });
245
+
246
+ // Because updating the country can update the required fields, we need to re-set form
247
+ // validity after render
248
+ requestAnimationFrame(() =>
249
+ this.setState({
250
+ isFormValid: this.checkValidity()
251
+ })
252
+ );
207
253
  }
208
254
 
209
255
  handleLearnMore() {
@@ -212,9 +258,20 @@ export default class J2sForm extends LightningElementWithState<J2sFormState> {
212
258
  ?.classList.toggle("hidden");
213
259
  }
214
260
 
215
- private checkValidity(country: string) {
261
+ private updateInnerHtml(selector: string, html: string): void {
262
+ const element = this.template.querySelector(selector);
263
+ if (element) {
264
+ element.innerHTML = html;
265
+ }
266
+ }
267
+
268
+ private isParticipatingCountry(country: string) {
269
+ return this.participatingCountryCodes.includes(country);
270
+ }
271
+
272
+ private checkValidity() {
216
273
  return (
217
- this.isParticipatingCountry(country) &&
274
+ this.isParticipatingCountry(this.state.country) &&
218
275
  this.requiredFormFields.every(
219
276
  (requiredFormField) =>
220
277
  // @ts-ignore