@kiva/kv-components 3.23.0 → 3.24.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
@@ -3,6 +3,28 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [3.24.0](https://github.com/kiva/kv-ui-elements/compare/@kiva/kv-components@3.23.1...@kiva/kv-components@3.24.0) (2023-06-21)
7
+
8
+
9
+ ### Features
10
+
11
+ * add support for $5 notes default lend CTA dropdown selected amount changing for ERL ([bac6944](https://github.com/kiva/kv-ui-elements/commit/bac69444b032c9b42c25f6359baf7c4a03eb651b))
12
+
13
+
14
+
15
+
16
+
17
+ ## [3.23.1](https://github.com/kiva/kv-ui-elements/compare/@kiva/kv-components@3.23.0...@kiva/kv-components@3.23.1) (2023-06-14)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * **KvLightbox:** use all available space for title instead of sharing with close button ([21db7b9](https://github.com/kiva/kv-ui-elements/commit/21db7b905119276e2fe21d4e31b4b73bc1e780d1))
23
+
24
+
25
+
26
+
27
+
6
28
  # [3.23.0](https://github.com/kiva/kv-ui-elements/compare/@kiva/kv-components@3.22.3...@kiva/kv-components@3.23.0) (2023-06-09)
7
29
 
8
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kiva/kv-components",
3
- "version": "3.23.0",
3
+ "version": "3.24.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -75,5 +75,5 @@
75
75
  "optional": true
76
76
  }
77
77
  },
78
- "gitHead": "097c378d83450baa4999ee5440a3862aa1496836"
78
+ "gitHead": "37410d19aa27c86c01a93e6423439b4da500673b"
79
79
  }
@@ -0,0 +1,313 @@
1
+ import {
2
+ isBetween25And50,
3
+ isLessThan25,
4
+ getLendCtaSelectedOption,
5
+ ERL_COOKIE_NAME,
6
+ TOP_UP_CAMPAIGN,
7
+ BASE_CAMPAIGN,
8
+ } from '../../../../utils/loanUtils';
9
+
10
+ describe('loanUtils', () => {
11
+ describe('isBetween25And50', () => {
12
+ it('should handle empty string', () => {
13
+ expect(isBetween25And50('')).toBe(false);
14
+ expect(isBetween25And50(undefined)).toBe(false);
15
+ expect(isBetween25And50(null)).toBe(false);
16
+ });
17
+
18
+ it('should return false for number below 25', () => {
19
+ expect(isBetween25And50('3')).toBe(false);
20
+ });
21
+
22
+ it('should return false for 25', () => {
23
+ expect(isBetween25And50('25')).toBe(false);
24
+ });
25
+
26
+ it('should return true for number between 25 and 50', () => {
27
+ expect(isBetween25And50('30')).toBe(true);
28
+ });
29
+
30
+ it('should return true for 50', () => {
31
+ expect(isBetween25And50('50')).toBe(true);
32
+ });
33
+
34
+ it('should return false for number above 50', () => {
35
+ expect(isBetween25And50('100')).toBe(false);
36
+ });
37
+ });
38
+
39
+ describe('isLessThan25', () => {
40
+ it('should handle empty string', () => {
41
+ expect(isLessThan25('')).toBe(false);
42
+ expect(isLessThan25(undefined)).toBe(false);
43
+ expect(isLessThan25(null)).toBe(false);
44
+ });
45
+
46
+ it('should return false for number below 0', () => {
47
+ expect(isLessThan25('-1')).toBe(false);
48
+ });
49
+
50
+ it('should return false for 0', () => {
51
+ expect(isLessThan25('0')).toBe(false);
52
+ });
53
+
54
+ it('should return true for number below 25', () => {
55
+ expect(isLessThan25('3')).toBe(true);
56
+ });
57
+
58
+ it('should return false for 25', () => {
59
+ expect(isLessThan25('25')).toBe(false);
60
+ });
61
+
62
+ it('should return true for number above 25', () => {
63
+ expect(isLessThan25('30')).toBe(false);
64
+ });
65
+ });
66
+
67
+ describe('getLendCtaSelectedOption', () => {
68
+ let mockCookieStoreGet;
69
+ let mockCookieStoreSet;
70
+ const mockTomorrow = new Date(2023, 1, 2);
71
+
72
+ beforeEach(() => {
73
+ mockCookieStoreGet = jest.fn();
74
+ mockCookieStoreSet = jest.fn();
75
+ jest.useFakeTimers('modern');
76
+ jest.setSystemTime(new Date(2023, 1, 1));
77
+ });
78
+
79
+ afterEach(() => {
80
+ jest.clearAllMocks();
81
+ jest.useRealTimers();
82
+ });
83
+
84
+ it('should handle unreserved amount greater than $50 without campaign', () => {
85
+ const result = getLendCtaSelectedOption(
86
+ mockCookieStoreGet,
87
+ mockCookieStoreSet,
88
+ true,
89
+ undefined,
90
+ '75.00',
91
+ '0.00',
92
+ );
93
+
94
+ expect(result).toBe('25');
95
+ expect(mockCookieStoreGet).toHaveBeenCalledWith(ERL_COOKIE_NAME);
96
+ expect(mockCookieStoreSet).toHaveBeenCalledTimes(0);
97
+ });
98
+
99
+ it('should handle unreserved amount between $25 and $50 without $5 notes', () => {
100
+ const result = getLendCtaSelectedOption(
101
+ mockCookieStoreGet,
102
+ mockCookieStoreSet,
103
+ true,
104
+ undefined,
105
+ '45.00',
106
+ '0.00',
107
+ );
108
+
109
+ expect(result).toBe('45');
110
+ expect(mockCookieStoreGet).toHaveBeenCalledWith(ERL_COOKIE_NAME);
111
+ expect(mockCookieStoreSet).toHaveBeenCalledTimes(0);
112
+ });
113
+
114
+ it('should handle unreserved amount less than $25 without $5 notes', () => {
115
+ const result = getLendCtaSelectedOption(
116
+ mockCookieStoreGet,
117
+ mockCookieStoreSet,
118
+ true,
119
+ undefined,
120
+ '15.00',
121
+ '0.00',
122
+ );
123
+
124
+ expect(result).toBe('15');
125
+ expect(mockCookieStoreGet).toHaveBeenCalledWith(ERL_COOKIE_NAME);
126
+ expect(mockCookieStoreSet).toHaveBeenCalledTimes(0);
127
+ });
128
+
129
+ it('should handle $5 notes ERL top up campaign without existing cookie', () => {
130
+ const result = getLendCtaSelectedOption(
131
+ mockCookieStoreGet,
132
+ mockCookieStoreSet,
133
+ true,
134
+ TOP_UP_CAMPAIGN,
135
+ '15.00',
136
+ '0.00',
137
+ );
138
+
139
+ expect(result).toBe('5');
140
+ expect(mockCookieStoreGet).toHaveBeenCalledWith(ERL_COOKIE_NAME);
141
+ expect(mockCookieStoreSet).toHaveBeenCalledWith(
142
+ ERL_COOKIE_NAME,
143
+ TOP_UP_CAMPAIGN,
144
+ { expires: mockTomorrow },
145
+ );
146
+ });
147
+
148
+ it('should handle $5 notes ERL base campaign without existing cookie', () => {
149
+ const result = getLendCtaSelectedOption(
150
+ mockCookieStoreGet,
151
+ mockCookieStoreSet,
152
+ true,
153
+ BASE_CAMPAIGN,
154
+ '15.00',
155
+ '10.00',
156
+ );
157
+
158
+ expect(result).toBe('10');
159
+ expect(mockCookieStoreGet).toHaveBeenCalledWith(ERL_COOKIE_NAME);
160
+ expect(mockCookieStoreSet).toHaveBeenCalledWith(
161
+ ERL_COOKIE_NAME,
162
+ BASE_CAMPAIGN,
163
+ { expires: mockTomorrow },
164
+ );
165
+ });
166
+
167
+ it('should handle $5 notes ERL base campaign max $25', () => {
168
+ const result = getLendCtaSelectedOption(
169
+ mockCookieStoreGet,
170
+ mockCookieStoreSet,
171
+ true,
172
+ BASE_CAMPAIGN,
173
+ '75.00',
174
+ '50.00',
175
+ );
176
+
177
+ expect(result).toBe('25');
178
+ expect(mockCookieStoreGet).toHaveBeenCalledWith(ERL_COOKIE_NAME);
179
+ expect(mockCookieStoreSet).toHaveBeenCalledWith(
180
+ ERL_COOKIE_NAME,
181
+ BASE_CAMPAIGN,
182
+ { expires: mockTomorrow },
183
+ );
184
+ });
185
+
186
+ it('should handle $5 notes ERL base campaign default to $5 with no balance', () => {
187
+ const result = getLendCtaSelectedOption(
188
+ mockCookieStoreGet,
189
+ mockCookieStoreSet,
190
+ true,
191
+ BASE_CAMPAIGN,
192
+ '15.00',
193
+ '0.00',
194
+ );
195
+
196
+ expect(result).toBe('5');
197
+ expect(mockCookieStoreGet).toHaveBeenCalledWith(ERL_COOKIE_NAME);
198
+ expect(mockCookieStoreSet).toHaveBeenCalledWith(
199
+ ERL_COOKIE_NAME,
200
+ BASE_CAMPAIGN,
201
+ { expires: mockTomorrow },
202
+ );
203
+ });
204
+
205
+ it('should handle $5 notes ERL base campaign default to unreserved amount when not enough', () => {
206
+ const result = getLendCtaSelectedOption(
207
+ mockCookieStoreGet,
208
+ mockCookieStoreSet,
209
+ true,
210
+ BASE_CAMPAIGN,
211
+ '5.00',
212
+ '15.00',
213
+ );
214
+
215
+ expect(result).toBe('5');
216
+ expect(mockCookieStoreGet).toHaveBeenCalledWith(ERL_COOKIE_NAME);
217
+ expect(mockCookieStoreSet).toHaveBeenCalledWith(
218
+ ERL_COOKIE_NAME,
219
+ BASE_CAMPAIGN,
220
+ { expires: mockTomorrow },
221
+ );
222
+ });
223
+
224
+ it('should handle $5 notes ERL top up campaign with existing cookie', () => {
225
+ mockCookieStoreGet.mockReturnValue(TOP_UP_CAMPAIGN);
226
+
227
+ const result = getLendCtaSelectedOption(
228
+ mockCookieStoreGet,
229
+ mockCookieStoreSet,
230
+ true,
231
+ undefined,
232
+ '15.00',
233
+ '0.00',
234
+ );
235
+
236
+ expect(result).toBe('5');
237
+ expect(mockCookieStoreGet).toHaveBeenCalledWith(ERL_COOKIE_NAME);
238
+ expect(mockCookieStoreSet).toHaveBeenCalledTimes(0);
239
+ });
240
+
241
+ it('should handle $5 notes ERL base campaign with existing cookie', () => {
242
+ mockCookieStoreGet.mockReturnValue(BASE_CAMPAIGN);
243
+
244
+ const result = getLendCtaSelectedOption(
245
+ mockCookieStoreGet,
246
+ mockCookieStoreSet,
247
+ true,
248
+ undefined,
249
+ '15.00',
250
+ '15.00',
251
+ );
252
+
253
+ expect(result).toBe('15');
254
+ expect(mockCookieStoreGet).toHaveBeenCalledWith(ERL_COOKIE_NAME);
255
+ expect(mockCookieStoreSet).toHaveBeenCalledTimes(0);
256
+ });
257
+
258
+ it('should handle $5 notes ERL campaign use partial string match', () => {
259
+ const result = getLendCtaSelectedOption(
260
+ mockCookieStoreGet,
261
+ mockCookieStoreSet,
262
+ true,
263
+ `asd${TOP_UP_CAMPAIGN}asd`,
264
+ '15.00',
265
+ '0.00',
266
+ );
267
+
268
+ expect(result).toBe('5');
269
+ expect(mockCookieStoreGet).toHaveBeenCalledWith(ERL_COOKIE_NAME);
270
+ expect(mockCookieStoreSet).toHaveBeenCalledWith(
271
+ ERL_COOKIE_NAME,
272
+ TOP_UP_CAMPAIGN,
273
+ { expires: mockTomorrow },
274
+ );
275
+ });
276
+
277
+ it('should handle $5 notes ERL campaign use case insensitive match', () => {
278
+ const result = getLendCtaSelectedOption(
279
+ mockCookieStoreGet,
280
+ mockCookieStoreSet,
281
+ true,
282
+ `asd${TOP_UP_CAMPAIGN.toLowerCase()}asd`,
283
+ '15.00',
284
+ '0.00',
285
+ );
286
+
287
+ expect(result).toBe('5');
288
+ expect(mockCookieStoreGet).toHaveBeenCalledWith(ERL_COOKIE_NAME);
289
+ expect(mockCookieStoreSet).toHaveBeenCalledWith(
290
+ ERL_COOKIE_NAME,
291
+ TOP_UP_CAMPAIGN,
292
+ { expires: mockTomorrow },
293
+ );
294
+ });
295
+
296
+ it('should handle $5 notes ERL campaign with undefined balance', () => {
297
+ mockCookieStoreGet.mockReturnValue(BASE_CAMPAIGN);
298
+
299
+ const result = getLendCtaSelectedOption(
300
+ mockCookieStoreGet,
301
+ mockCookieStoreSet,
302
+ true,
303
+ undefined,
304
+ '100.00',
305
+ undefined,
306
+ );
307
+
308
+ expect(result).toBe('25');
309
+ expect(mockCookieStoreGet).toHaveBeenCalledTimes(0);
310
+ expect(mockCookieStoreSet).toHaveBeenCalledTimes(0);
311
+ });
312
+ });
313
+ });
@@ -0,0 +1,88 @@
1
+ export const ERL_COOKIE_NAME = 'kverlfivedollarnotes';
2
+ export const TOP_UP_CAMPAIGN = 'TOPUP-VB-BALANCE-MPV1';
3
+ export const BASE_CAMPAIGN = 'BASE-VB_BALANCE_MPV1';
4
+
5
+ /**
6
+ * Checks if the unreserved amount is between 25 and 50
7
+ *
8
+ * @param {string} unreservedAmount
9
+ * @returns Whether the unreserved amount is between 25 and 50
10
+ */
11
+ export function isBetween25And50(unreservedAmount) {
12
+ return unreservedAmount <= 50 && unreservedAmount > 25;
13
+ }
14
+
15
+ /**
16
+ * Checks if the unreserved amount is less than 25
17
+ *
18
+ * @param {string} unreservedAmount
19
+ * @returns Whether the unreserved amount is less than 25
20
+ */
21
+ export function isLessThan25(unreservedAmount) {
22
+ return unreservedAmount < 25 && unreservedAmount > 0;
23
+ }
24
+
25
+ /**
26
+ * Gets the selected option for the Lend CTA component
27
+ *
28
+ * @param {Function} getCookie Method that returns a cookie by name
29
+ * @param {Function} setCookie Method that sets a cookie with the provided name, value, and options
30
+ * @param {boolean} enableFiveDollarsNotes Whether $5 notes experiment is assigned
31
+ * @param {string} campaign The "utm_campaign" query param sourced from the Vue component route
32
+ * @param {string} unreservedAmount The unreserved amount for the loan
33
+ * @param {string} userBalance The balance of the current user
34
+ * @returns {string} The option to be selected in the CTA dropdown
35
+ */
36
+ export function getLendCtaSelectedOption(
37
+ getCookie,
38
+ setCookie,
39
+ enableFiveDollarsNotes,
40
+ campaign,
41
+ unreservedAmount,
42
+ userBalance,
43
+ ) {
44
+ // Don't enable the campaign changes when the user balance is undefined (user not logged in)
45
+ if (enableFiveDollarsNotes && typeof userBalance !== 'undefined') {
46
+ let currentCampaign = getCookie?.(ERL_COOKIE_NAME);
47
+
48
+ if (campaign && typeof campaign === 'string' && !currentCampaign) {
49
+ // Effects of the campaign lasts for 24 hours
50
+ const expires = new Date();
51
+ expires.setHours(expires.getHours() + 24);
52
+
53
+ const campaignToCheck = campaign.toUpperCase();
54
+
55
+ // eslint-disable-next-line no-nested-ternary
56
+ currentCampaign = campaignToCheck.includes(TOP_UP_CAMPAIGN)
57
+ ? TOP_UP_CAMPAIGN
58
+ : (campaignToCheck.includes(BASE_CAMPAIGN) ? BASE_CAMPAIGN : '');
59
+
60
+ if (currentCampaign && setCookie) {
61
+ setCookie(ERL_COOKIE_NAME, currentCampaign, { expires });
62
+ }
63
+ }
64
+
65
+ if (currentCampaign) {
66
+ // Base campaign gets largest increment of $5 under the user's balance up to $25 or the unreserved amount
67
+ if (currentCampaign === BASE_CAMPAIGN) {
68
+ let val = Math.floor(userBalance / 5) * 5;
69
+
70
+ // eslint-disable-next-line no-nested-ternary
71
+ val = val === 0 ? 5 : (val > 25 ? 25 : val);
72
+
73
+ return Number(val <= unreservedAmount ? val : unreservedAmount).toFixed();
74
+ }
75
+
76
+ // Top up campaign defaults to $5
77
+ return Number(unreservedAmount > 5 ? 5 : unreservedAmount).toFixed();
78
+ }
79
+ }
80
+
81
+ // Handle when $5 notes isn't enabled
82
+ if (isBetween25And50(unreservedAmount) || isLessThan25(unreservedAmount)) {
83
+ return Number(unreservedAmount).toFixed();
84
+ }
85
+
86
+ // $25 is the fallback default selected option
87
+ return '25';
88
+ }
@@ -205,6 +205,10 @@
205
205
  :show-view-loan="showViewLoan"
206
206
  :custom-loan-details="customLoanDetails"
207
207
  :external-links="externalLinks"
208
+ :route="route"
209
+ :user-balance="userBalance"
210
+ :get-cookie="getCookie"
211
+ :set-cookie="setCookie"
208
212
  class="tw-mt-auto"
209
213
  :class="{ 'tw-w-full' : unreservedAmount <= 0 }"
210
214
  @add-to-basket="$emit('add-to-basket', $event)"
@@ -304,6 +308,22 @@ export default {
304
308
  type: Boolean,
305
309
  default: false,
306
310
  },
311
+ route: {
312
+ type: String,
313
+ default: undefined,
314
+ },
315
+ userBalance: {
316
+ type: String,
317
+ default: undefined,
318
+ },
319
+ getCookie: {
320
+ type: Function,
321
+ default: undefined,
322
+ },
323
+ setCookie: {
324
+ type: Function,
325
+ default: undefined,
326
+ },
307
327
  },
308
328
  data() {
309
329
  return {
@@ -1,8 +1,5 @@
1
1
  <template>
2
- <kv-button
3
- :state="buttonState"
4
- @click="addToBasket"
5
- >
2
+ <kv-button @click="addToBasket">
6
3
  {{ buttonText }}
7
4
  </kv-button>
8
5
  </template>
package/vue/KvLendCta.vue CHANGED
@@ -132,6 +132,7 @@ import KvLendAmountButton from './KvLendAmountButton.vue';
132
132
  import KvUiSelect from './KvSelect.vue';
133
133
  import KvUiButton from './KvButton.vue';
134
134
  import KvMaterialIcon from './KvMaterialIcon.vue';
135
+ import { getLendCtaSelectedOption } from '../utils/loanUtils';
135
136
 
136
137
  export default {
137
138
  name: 'KvLendCta',
@@ -178,11 +179,34 @@ export default {
178
179
  type: Boolean,
179
180
  default: false,
180
181
  },
182
+ route: {
183
+ type: String,
184
+ default: undefined,
185
+ },
186
+ userBalance: {
187
+ type: String,
188
+ default: undefined,
189
+ },
190
+ getCookie: {
191
+ type: Function,
192
+ default: undefined,
193
+ },
194
+ setCookie: {
195
+ type: Function,
196
+ default: undefined,
197
+ },
181
198
  },
182
199
  data() {
183
200
  return {
184
201
  mdiChevronRight,
185
- selectedOption: this.getSelectedOption(this.loan?.unreservedAmount),
202
+ selectedOption: getLendCtaSelectedOption(
203
+ this.getCookie,
204
+ this.setCookie,
205
+ this.enableFiveDollarsNotes,
206
+ this.route?.query?.utm_campaign,
207
+ this.loan?.unreservedAmount,
208
+ this.userBalance,
209
+ ),
186
210
  };
187
211
  },
188
212
  computed: {
@@ -313,7 +337,14 @@ export default {
313
337
  watch: {
314
338
  unreservedAmount(newValue, previousValue) {
315
339
  if (newValue !== previousValue && previousValue === '') {
316
- this.selectedOption = this.getSelectedOption(newValue);
340
+ this.selectedOption = getLendCtaSelectedOption(
341
+ this.getCookie,
342
+ this.setCookie,
343
+ this.enableFiveDollarsNotes,
344
+ this.route?.query?.utm_campaign,
345
+ newValue,
346
+ this.userBalance,
347
+ );
317
348
  }
318
349
  },
319
350
  },
@@ -337,12 +368,6 @@ export default {
337
368
  isAmountBetween25And500(unreservedAmount) {
338
369
  return unreservedAmount < 500 && unreservedAmount >= 25;
339
370
  },
340
- getSelectedOption(unreservedAmount) {
341
- if (this.isAmountBetween25And50(unreservedAmount) || this.isAmountLessThan25(unreservedAmount)) {
342
- return Number(unreservedAmount).toFixed();
343
- }
344
- return '25';
345
- },
346
371
  trackLendAmountSelection(selectedDollarAmount) {
347
372
  this.kvTrackFunction(
348
373
  'Lending',
@@ -63,7 +63,7 @@
63
63
  tw-p-2.5 md:tw-px-4 md:tw-pt-4 md:tw-pb-3.5
64
64
  "
65
65
  >
66
- <div>
66
+ <div class="tw-flex-grow">
67
67
  <!-- @slot header -->
68
68
  <slot name="header">
69
69
  <h2 class="tw-text-h3 tw-flex-1">