@kompasid/lit-web-components 0.9.28 → 0.9.29

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.
@@ -0,0 +1,500 @@
1
+ import { html, css, LitElement, nothing } from 'lit'
2
+ import { customElement, state, property } from 'lit/decorators.js'
3
+ import { unsafeSVG } from 'lit/directives/unsafe-svg.js'
4
+ import { unsafeHTML } from 'lit/directives/unsafe-html.js'
5
+ import { TWStyles } from '../../../tailwind/tailwind.js'
6
+ import { PackageData, OfferingItem } from './types.js'
7
+ import { getFontAwesomeIcon } from '../../utils/fontawesome-setup.js'
8
+ import { formatRupiah } from '../../utils/formatRupiah.js'
9
+
10
+ @customElement('kompasid-metered-paywall-personalize')
11
+ export class KompasMeteredPaywallPersonalize extends LitElement {
12
+ static styles = [
13
+ css`
14
+ .text-transition {
15
+ width: 100%;
16
+ height: 5rem;
17
+ }
18
+
19
+ .body {
20
+ position: sticky;
21
+ top: 0;
22
+ height: 100%;
23
+ width: 100%;
24
+ }
25
+
26
+ .message-collapse-engage-returners span {
27
+ color: #ff7a00;
28
+ font-weight: 700;
29
+ text-wrap: nowrap;
30
+ }
31
+
32
+ .message-collapse-engage-returners s {
33
+ color: #999999;
34
+ text-wrap: nowrap;
35
+ }
36
+
37
+ .font-lora {
38
+ font-family: 'Lora', 'Georgia', 'serif';
39
+ }
40
+
41
+ .icon {
42
+ height: 1rem;
43
+ color: #48bb78;
44
+ }
45
+
46
+ .icon.lg {
47
+ height: 1.5rem;
48
+ }
49
+
50
+ .icon.lg svg {
51
+ height: 1.5rem;
52
+ }
53
+ `,
54
+ TWStyles,
55
+ ]
56
+
57
+ @state() private isLoading: Boolean = true
58
+ @state() private maxQuota: number = 3
59
+ @state() private packageData: PackageData | undefined
60
+
61
+ /**
62
+ * Props
63
+ */
64
+ /**
65
+ * prop countdownArticle untuk menghandle sudah berapa artikel gratis yang user baca.
66
+ * prop segment untuk menentukan paywall template dari segmen apa yang di pakai, bila tidak ada yang cocok jangan render paywall
67
+ * prop offering untuk handle offering yang akan di berikan, bila tidak di isi maka akan default menjadi Q1
68
+ * prop user_name untuk menerima nama user yang akan di tampilkan paywall specific
69
+ * prop paywall_location = The location where user encounter the paywall
70
+ * prop paywall_subscription_package = The name of the subscription package viewed by user
71
+ * prop paywall_subscription_id = The ID of the subscription package viewed by user
72
+ * prop paywall_subscription_price = The price of the subscriprtion package viewed by user
73
+ * prop paywall_position = The position of ther subscription package viewed by user
74
+ * prop tracker_page_type = Type of the page
75
+ * prop tracker_content_id = ID of article (slug)
76
+ * prop tracker_content_type = Whether it's free article or paid article
77
+ * prop tracker_content_title = The title of article
78
+ * prop tracker_content_categories = The category of the content
79
+ * prop tracker_user_type = Type of user based on their subscription
80
+ * prop tracker_subscription_status = Status of their subscription
81
+ * prop tracker_page_domain = Page Domain
82
+ * prop tracker_metered_wall_type = The type of Metered Wall
83
+ * prop tracker_epaper_edition = The edition of epaper viewed by user
84
+ * prop tracker_metered_wall_balance = The balance of their metered wall
85
+ */
86
+ @property({ type: Number }) countdownArticle = 0
87
+ @property({ type: String }) segment = ''
88
+ @property({ type: String }) offering = ''
89
+ @property({ type: String }) user_name = ''
90
+ @property({ type: String }) paywall_location = ''
91
+ @property({ type: String }) paywall_subscription_package = ''
92
+ @property({ type: String }) paywall_subscription_id = ''
93
+ @property({ type: Number }) paywall_subscription_price = 0
94
+ @property({ type: Number }) paywall_position = 0
95
+ @property({ type: String }) tracker_page_type = ''
96
+ @property({ type: String }) tracker_content_id = ''
97
+ @property({ type: String }) tracker_content_title = ''
98
+ @property({ type: String }) tracker_content_categories = ''
99
+ @property({ type: String }) tracker_content_type = ''
100
+ @property({ type: String }) tracker_user_type = ''
101
+ @property({ type: String }) tracker_subscription_status = ''
102
+ @property({ type: String }) tracker_page_domain = ''
103
+ @property({ type: String }) tracker_metered_wall_type = ''
104
+ @property({ type: Number }) tracker_metered_wall_balance = 0
105
+
106
+ override async connectedCallback() {
107
+ super.connectedCallback()
108
+ await this.getMeteredPaywallData()
109
+ this.dataLayeronMeteredPaywall()
110
+ }
111
+
112
+ async getMeteredPaywallData() {
113
+ try {
114
+ const getSegment = this.segment.toLocaleLowerCase().replace(' ', '_')
115
+ const response = await fetch(
116
+ `https://cdn-www.kompas.id/web-component/metered-paywall-personalize/${getSegment}.json`
117
+ )
118
+ const json = await response.json()
119
+
120
+ this.packageData = json
121
+ } catch (error) {
122
+ throw Error('Failed to get metered paywall data')
123
+ } finally {
124
+ this.isLoading = false
125
+ }
126
+ }
127
+
128
+ private redirectToBerlangganan() {
129
+ this.dataLayeronLanggananButton()
130
+
131
+ // Pastikan data tersedia
132
+ const offeringKey = this.offering || 'Q1'
133
+ const offeringData = this.packageData?.offering?.[offeringKey]
134
+
135
+ const checkoutUrl = offeringData?.checkout_url || ''
136
+
137
+ // Encode referrer dengan aman
138
+ const { origin, pathname, search } = window.location
139
+ const referrer = encodeURIComponent(`${origin}${pathname}${search}`)
140
+
141
+ // Bangun URL dengan parameter tambahan aman
142
+ const url = new URL(checkoutUrl)
143
+ url.searchParams.set('referrer', referrer)
144
+
145
+ // Redirect ke URL akhir
146
+ window.location.href = url.toString()
147
+ }
148
+
149
+ private dataLayeronLanggananButton() {
150
+ window.dataLayer.push({
151
+ event: 'subscribe_button_clicked',
152
+ paywall_location: this.paywall_location || '',
153
+ paywall_subscription_package: this.paywall_subscription_package || '',
154
+ paywall_subscription_id: this.paywall_subscription_id || '',
155
+ paywall_subscription_price: this.paywall_subscription_price || 0,
156
+ paywall_position: this.paywall_position || 0,
157
+ page_type: this.tracker_page_type,
158
+ content_id: this.tracker_content_id,
159
+ content_title: this.tracker_content_title,
160
+ content_categories: this.tracker_content_categories,
161
+ content_type: this.tracker_content_type,
162
+ user_type: this.tracker_user_type || 'R',
163
+ subscription_status: this.tracker_subscription_status,
164
+ page_domain: this.tracker_page_domain || 'Kompas.id',
165
+ metered_wall_type: this.tracker_metered_wall_type || 'MP',
166
+ metered_wall_balance: this.maxQuota - this.countdownArticle + 1,
167
+ paywall_segment: this.segment || '',
168
+ checkout_url:
169
+ this.packageData?.offering[this.offering ? this.offering : 'Q1']
170
+ .checkout_url,
171
+ })
172
+ }
173
+
174
+ private dataLayeronMeteredPaywall() {
175
+ window.dataLayer.push({
176
+ event: 'paywall_viewed',
177
+ paywall_location: this.paywall_location || '',
178
+ paywall_subscription_package: this.paywall_subscription_package || '',
179
+ paywall_subscription_id: this.paywall_subscription_id || '',
180
+ paywall_subscription_price: this.paywall_subscription_price || 0,
181
+ paywall_position: this.paywall_position || 0,
182
+ page_type: this.tracker_page_type,
183
+ content_id: this.tracker_content_id,
184
+ content_title: this.tracker_content_title,
185
+ content_categories: this.tracker_content_categories,
186
+ content_type: this.tracker_content_type,
187
+ user_type: this.tracker_user_type || 'R',
188
+ subscription_status: this.tracker_subscription_status,
189
+ page_domain: this.tracker_page_domain || 'Kompas.id',
190
+ metered_wall_type: this.tracker_metered_wall_type || 'MP',
191
+ metered_wall_balance: this.maxQuota - this.countdownArticle + 1,
192
+ paywall_segment: this.segment || '',
193
+ checkout_url:
194
+ this.packageData?.offering[this.offering ? this.offering : 'Q1']
195
+ .checkout_url,
196
+ })
197
+ }
198
+
199
+ private stateDefaultPaywall = false
200
+ private computedstateDefaultPaywall() {
201
+ this.stateDefaultPaywall = !this.stateDefaultPaywall
202
+ this.requestUpdate()
203
+ }
204
+
205
+ /**
206
+ * Ganti placeholder _key_ di template dengan nilai dari data offering
207
+ * Hanya replace jika placeholder cocok dan datanya valid
208
+ */
209
+ private replacePlaceholdersFromOffering(
210
+ template: string,
211
+ offeringData: OfferingItem
212
+ ): string {
213
+ if (!offeringData) return template
214
+ // regex cari placeholder seperti _harga_coret_, _offering_price_, dst.
215
+ return template.replace(/_([a-zA-Z0-9_]+)_/g, (match, key) => {
216
+ // cek apakah key ada di dalam data
217
+ if (key in offeringData) {
218
+ const value = (offeringData as any)[key]
219
+
220
+ // jika value number, auto format ke rupiah
221
+ if (typeof value === 'number') {
222
+ return formatRupiah(value)
223
+ }
224
+
225
+ // jika string, langsung return
226
+ return String(value)
227
+ }
228
+
229
+ // kalau placeholder tidak dikenal, biarkan apa adanya
230
+ return match
231
+ })
232
+ }
233
+
234
+ private engageReturners() {
235
+ const textTemplateFormater = this.replacePlaceholdersFromOffering(
236
+ this.packageData?.paywall.messageTitleCollapse || '',
237
+ this.packageData?.offering[this.offering ? this.offering : 'Q1'] ||
238
+ ({} as OfferingItem)
239
+ )
240
+ return !this.stateDefaultPaywall
241
+ ? html`
242
+ <div
243
+ class="fixed w-full inset-0 px-4 lg:px-0 h-full flex justify-center items-center bg-black bg-opacity-75 z-[999]"
244
+ >
245
+ <div
246
+ class="bg-white max-w-[460px] w-full justify-center items-center flex flex-col p-6 lg:pb-6 lg:px-6 lg:pt-9 relative rounded"
247
+ >
248
+ <button
249
+ class="font-bold cursor-pointer text-grey-400 flex rounded text-base absolute right-0 top-0 w-6 h-6 lg:w-8 lg:h-8 justify-center items-center mt-4 mr-4"
250
+ @click=${this.computedstateDefaultPaywall}
251
+ >
252
+ ${unsafeSVG(getFontAwesomeIcon('fa', 'times', 20, 20))}
253
+ </button>
254
+ <image
255
+ src="${this.packageData?.paywall.image}"
256
+ alt="content"
257
+ class="w-[248px] lg:w-[364px]"
258
+ />
259
+ <div
260
+ class="font-lora font-bold text-grey-600 lg:text-2xl text-center"
261
+ >
262
+ <div>Halo ${this.user_name},</div>
263
+ <div>${this.packageData?.paywall.messageTitleExpand}</div>
264
+ </div>
265
+ <div
266
+ class="pt-3 text-center font-sans text-grey-600 text-sm lg:text-base leading-none"
267
+ >
268
+ <div>${this.packageData?.paywall.descriptionExpand}</div>
269
+ <div class="text-base lg:text-lg text-grey-400 py-3">
270
+ <s
271
+ >${formatRupiah(
272
+ this.packageData?.offering[
273
+ this.offering ? this.offering : 'Q1'
274
+ ].harga_coret || 0
275
+ )}</s
276
+ >
277
+ <span class="text-lg lg:text-2xl text-orange-400 font-bold">
278
+ ${formatRupiah(
279
+ this.packageData?.offering[
280
+ this.offering ? this.offering : 'Q1'
281
+ ].offering_price || 0
282
+ )}
283
+ </span>
284
+ ${this.packageData?.offering[
285
+ this.offering ? this.offering : 'Q1'
286
+ ].duration
287
+ ? html`
288
+ <span class="text-base lg:text-lg text-grey-600"
289
+ >${this.packageData?.offering[
290
+ this.offering ? this.offering : 'Q1'
291
+ ].duration}</span
292
+ >
293
+ `
294
+ : nothing}
295
+ </div>
296
+ <div>untuk tahun pertama.</div>
297
+ </div>
298
+ <div class="w-full pt-6">
299
+ <button
300
+ @click="${this.redirectToBerlangganan}"
301
+ class="bg-brand-1 whitespace-nowrap rounded md:rounded h-8 lg:h-10 px-4 md:px-5 text-sm lg:text-base text-white font-bold leading-none w-full"
302
+ >
303
+ ${this.packageData?.offering[
304
+ this.offering ? this.offering : 'Q1'
305
+ ].checkout_text}
306
+ </button>
307
+ </div>
308
+ </div>
309
+ </div>
310
+ `
311
+ : html`
312
+ <div class="sticky bottom-0 w-full h-full">
313
+ <div
314
+ class="flex flex-col lg:flex-row w-full bg-blue-100 py-4 justify-center gap-4 px-4 lg:px-0 bottom-0"
315
+ >
316
+ <div
317
+ class="text-grey-600 text-base self-center text-left message-collapse-engage-returners font-bold"
318
+ >
319
+ ${unsafeHTML(textTemplateFormater)}
320
+ </div>
321
+ <div>
322
+ <button
323
+ @click="${this.redirectToBerlangganan}"
324
+ class="bg-brand-1 whitespace-nowrap rounded md:rounded h-10 px-4 md:px-5 text-base text-white font-bold leading-[18px] w-full lg:w-auto"
325
+ >
326
+ ${this.packageData?.offering[
327
+ this.offering ? this.offering : 'Q1'
328
+ ].checkout_text}
329
+ </button>
330
+ </div>
331
+ </div>
332
+ </div>
333
+ `
334
+ }
335
+
336
+ private passiveFaders() {
337
+ const messageTitleCollapse = this.replacePlaceholdersFromOffering(
338
+ this.packageData?.paywall.messageTitleCollapse || '',
339
+ this.packageData?.offering[this.offering ? this.offering : 'Q1'] ||
340
+ ({} as OfferingItem)
341
+ )
342
+ const descriptionExpand = this.replacePlaceholdersFromOffering(
343
+ this.packageData?.paywall.descriptionExpand || '',
344
+ this.packageData?.offering[this.offering ? this.offering : 'Q1'] ||
345
+ ({} as OfferingItem)
346
+ )
347
+ return html`
348
+ <div class="sticky bottom-0 w-full h-full">
349
+ <div
350
+ class="flex flex-col lg:flex-row w-full bg-blue-100 py-4 gap-4 px-4 bottom-0"
351
+ >
352
+ <div
353
+ class="flex flex-col lg:flex-row w-full justify-between max-w-[1200px] gap-4 mx-auto "
354
+ >
355
+ ${!this.stateDefaultPaywall
356
+ ? html`
357
+ <div class="h-9 w-9 hidden lg:flex"></div>
358
+ <div
359
+ class="flex flex-col lg:flex-row justify-center items-center lg:items-start lg:justify-between w-full max-w-5xl"
360
+ >
361
+ <div
362
+ class="text-grey-600 font-lora message-collapse-engage-returners order-2"
363
+ >
364
+ <div
365
+ class="text-xl lg:text-2xl font-bold mb-3 text-center lg:text-start"
366
+ >
367
+ ${this.packageData?.paywall.messageTitleExpand}
368
+ </div>
369
+ <div class="flex flex-col font-sans gap-3 mb-3">
370
+ ${this.packageData?.paywall.listBenefits
371
+ ? this.packageData?.paywall.listBenefits.map(
372
+ item =>
373
+ html`
374
+ <div
375
+ class="flex items-baseline align-baseline"
376
+ >
377
+ <div
378
+ class="bg-white text-green-500 h-3 w-3 mr-1 rounded-full flex justify-center items-center"
379
+ >
380
+ ${unsafeSVG(
381
+ getFontAwesomeIcon(
382
+ 'fas',
383
+ 'circle-check',
384
+ 12,
385
+ 12
386
+ )
387
+ )}
388
+ </div>
389
+ <h6 class="text-base ml-1">${item}</h6>
390
+ </div>
391
+ `
392
+ )
393
+ : nothing}
394
+ </div>
395
+ <div
396
+ class="text-grey-600 font-sans text-sm lg:text-base self-center text-left message-collapse-engage-returners mb-4"
397
+ >
398
+ ${unsafeHTML(descriptionExpand)}
399
+ </div>
400
+ <div>
401
+ <button
402
+ @click="${this.redirectToBerlangganan}"
403
+ class="bg-brand-1 flex whitespace-nowrap items-center justify-center rounded md:rounded h-10 px-4 md:px-5 text-base text-white font-bold leading-[18px] w-full lg:w-auto"
404
+ >
405
+ ${this.packageData?.offering[
406
+ this.offering ? this.offering : 'Q1'
407
+ ].checkout_text}
408
+ </button>
409
+ </div>
410
+ </div>
411
+ <div
412
+ class="flex self-center mt-10 mb-3 lg:mb-0 lg:mt-0 lg:ml-6 order-1 lg:order-2"
413
+ >
414
+ <image
415
+ src="${this.packageData?.paywall.image}"
416
+ alt="content"
417
+ class="w-[112px] lg:w-[202px]"
418
+ />
419
+ </div>
420
+ </div>
421
+ <div>
422
+ <button
423
+ @click="${this.computedstateDefaultPaywall}"
424
+ class="h-9 w-9 absolute lg:static flex items-center justify-center text-blue-500 rounded bg-blue-200 right-4 top-4"
425
+ >
426
+ <div>
427
+ ${unsafeSVG(getFontAwesomeIcon('fas', 'chevron-down'))}
428
+ </div>
429
+ </button>
430
+ </div>
431
+ `
432
+ : html`
433
+ <div class="h-9 w-9 flex"></div>
434
+ <div class="flex justify-between">
435
+ <div
436
+ class="text-grey-600 text-base self-center text-left message-collapse-engage-returners font-bold"
437
+ >
438
+ ${unsafeHTML(messageTitleCollapse)}
439
+ </div>
440
+ <div class="hidden lg:flex ml-6">
441
+ <button
442
+ @click="${this.redirectToBerlangganan}"
443
+ class="bg-brand-1 whitespace-nowrap rounded md:rounded h-10 px-4 md:px-5 text-base text-white font-bold leading-[18px] w-full lg:w-auto"
444
+ >
445
+ ${this.packageData?.offering[
446
+ this.offering ? this.offering : 'Q1'
447
+ ].checkout_text}
448
+ </button>
449
+ </div>
450
+ <div class="flex lg:hidden ml-4">
451
+ <button
452
+ @click="${this.computedstateDefaultPaywall}"
453
+ class="h-9 w-9 flex items-center justify-center text-blue-500 rounded bg-blue-200"
454
+ >
455
+ <div>
456
+ ${unsafeSVG(getFontAwesomeIcon('fas', 'chevron-up'))}
457
+ </div>
458
+ </button>
459
+ </div>
460
+ </div>
461
+ <div>
462
+ <button
463
+ @click="${this.computedstateDefaultPaywall}"
464
+ class="h-9 w-9 hidden lg:flex items-center justify-center text-blue-500 rounded bg-blue-200"
465
+ >
466
+ <div>
467
+ ${unsafeSVG(getFontAwesomeIcon('fas', 'chevron-up'))}
468
+ </div>
469
+ </button>
470
+ <button
471
+ @click="${this.redirectToBerlangganan}"
472
+ class="bg-brand-1 lg:hidden flex whitespace-nowrap items-center justify-center rounded md:rounded h-10 px-4 md:px-5 text-base text-white font-bold leading-[18px] w-full"
473
+ >
474
+ ${this.packageData?.offering[
475
+ this.offering ? this.offering : 'Q1'
476
+ ].checkout_text}
477
+ </button>
478
+ </div>
479
+ `}
480
+ </div>
481
+ </div>
482
+ </div>
483
+ `
484
+ }
485
+
486
+ private pickTemplate() {
487
+ switch (this.segment) {
488
+ case 'Engaged Returners':
489
+ return this.engageReturners()
490
+ case 'Passive Faders':
491
+ return this.passiveFaders()
492
+ default:
493
+ return nothing
494
+ }
495
+ }
496
+
497
+ render() {
498
+ return !this.isLoading ? this.pickTemplate() : nothing
499
+ }
500
+ }
@@ -0,0 +1,155 @@
1
+ # kompasid-metered-paywall-personalize
2
+
3
+ Komponen Web Component yang digunakan untuk menampilkan **Metered Paywall Personalize** pada situs **Kompas.id**.
4
+ Komponen ini menyesuaikan tampilan paywall berdasarkan **segmentasi pengguna** seperti *Engaged Returners* dan *Passive Faders*, serta mengirim event tracking ke **Google Tag Manager (GTM)**.
5
+
6
+ ---
7
+
8
+ ## 📦 Import
9
+
10
+ ```js
11
+ import 'kompasid-metered-paywall-personalize'
12
+ ```
13
+
14
+ ---
15
+
16
+ ## 🧩 Deskripsi Umum
17
+
18
+ Komponen ini akan:
19
+ - Menampilkan tampilan paywall berdasarkan segmentasi pengguna (`segment`).
20
+ - Mengambil data paywall dari endpoint CDN Kompas.
21
+ - Mengirim event `paywall_viewed` dan `subscribe_button_clicked` ke `window.dataLayer`.
22
+ - Melakukan personalisasi tampilan dan teks berdasarkan data offering (paket langganan).
23
+
24
+ ---
25
+
26
+ ## ⚙️ Properties
27
+
28
+ | Property | Attribute | Description | Type | Default |
29
+ | ------------------------------ | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------- |
30
+ | `countdownArticle` | `countdown-article` | Menghitung berapa artikel gratis yang telah dibaca user (untuk GTM tracking, tidak ditampilkan di UI). | `number` | `0` |
31
+ | `segment` | `segment` | Menentukan template paywall berdasarkan segmentasi user (`Engaged Returners`, `Passive Faders`, dll). | `string` | `''` |
32
+ | `offering` | `offering` | Menentukan paket penawaran langganan (default: `Q1`). | `string` | `''` |
33
+ | `user_name` | `user_name` | Nama pengguna untuk menampilkan sapaan personal (“Halo, [nama]”). | `string` | `''` |
34
+ | `paywall_location` | `paywall_location` | Lokasi di mana user menemukan paywall. | `string` | `''` |
35
+ | `paywall_subscription_package` | `paywall_subscription_package` | Nama paket langganan yang ditampilkan ke user. | `string` | `''` |
36
+ | `paywall_subscription_id` | `paywall_subscription_id` | ID paket langganan yang ditampilkan ke user. | `string` | `''` |
37
+ | `paywall_subscription_price` | `paywall_subscription_price` | Harga paket langganan yang ditampilkan ke user. | `number` | `0` |
38
+ | `paywall_position` | `paywall_position` | Posisi dari paket langganan yang sedang ditampilkan. | `number` | `0` |
39
+ | `tracker_page_type` | `tracker_page_type` | Jenis halaman (misalnya: `article`, `homepage`). | `string` | `''` |
40
+ | `tracker_content_id` | `tracker_content_id` | ID atau slug artikel. | `string` | `''` |
41
+ | `tracker_content_title` | `tracker_content_title` | Judul artikel yang sedang dibaca. | `string` | `''` |
42
+ | `tracker_content_categories` | `tracker_content_categories` | Kategori dari artikel yang sedang dibaca. | `string` | `''` |
43
+ | `tracker_content_type` | `tracker_content_type` | Jenis artikel (`free` atau `paid`). | `string` | `''` |
44
+ | `tracker_user_type` | `tracker_user_type` | Tipe user berdasarkan status langganan (`R` untuk regon, `S` untuk subscriber). | `string` | `''` |
45
+ | `tracker_subscription_status` | `tracker_subscription_status` | Status langganan user. | `string` | `''` |
46
+ | `tracker_page_domain` | `tracker_page_domain` | Domain halaman (default: `Kompas.id`). | `string` | `''` |
47
+ | `tracker_metered_wall_type` | `tracker_metered_wall_type` | Tipe paywall yang digunakan (contoh: `MP` = Metered Paywall). | `string` | `''` |
48
+ | `tracker_metered_wall_balance` | `tracker_metered_wall_balance` | Sisa kuota artikel gratis (calculated: `maxQuota - countdownArticle + 1`). | `number` | `0` |
49
+
50
+ ---
51
+
52
+ ## 🧠 State Internal
53
+
54
+ | State Name | Type | Default | Description |
55
+ | ----------------- | --------- | -------- | --------------------------------------------------------- |
56
+ | `isLoading` | `boolean` | `true` | Indikator apakah data JSON sudah dimuat. |
57
+ | `maxQuota` | `number` | `3` | Jumlah maksimum artikel gratis sebelum paywall muncul. |
58
+ | `packageData` | `object` | `undefined` | Data JSON hasil fetch dari CDN. |
59
+ | `stateDefaultPaywall` | `boolean` | `false` | Mengatur tampilan collapse/expand paywall. |
60
+
61
+ ---
62
+
63
+ ## 🧾 Lifecycle
64
+
65
+ ### `connectedCallback()`
66
+ - Dipanggil saat komponen dimasukkan ke DOM.
67
+ - Memanggil:
68
+ - `getMeteredPaywallData()` untuk fetch JSON paywall.
69
+ - `dataLayeronMeteredPaywall()` untuk push event “paywall_viewed”.
70
+
71
+ ---
72
+
73
+ ## 🔄 Method
74
+
75
+ | Method Name | Description |
76
+ | ------------ | ------------ |
77
+ | **`getMeteredPaywallData()`** | Fetch data JSON berdasarkan segment dari endpoint CDN. |
78
+ | **`redirectToBerlangganan()`** | Mengarahkan user ke halaman checkout berdasarkan URL di `offering`. |
79
+ | **`dataLayeronLanggananButton()`** | Mengirim event `subscribe_button_clicked` ke `window.dataLayer`. |
80
+ | **`dataLayeronMeteredPaywall()`** | Mengirim event `paywall_viewed` ke `window.dataLayer`. |
81
+ | **`replacePlaceholdersFromOffering(template, offeringData)`** | Melakukan replace placeholder seperti `_harga_coret_` di template dengan nilai dari `offeringData`. |
82
+ | **`engageReturners()`** | Template tampilan paywall untuk segment “Engaged Returners”. |
83
+ | **`passiveFaders()`** | Template tampilan paywall untuk segment “Passive Faders”. |
84
+ | **`pickTemplate()`** | Menentukan tampilan yang digunakan berdasarkan `segment`. |
85
+
86
+ ---
87
+
88
+ ## 🎨 Tampilan
89
+
90
+ Komponen menggunakan kombinasi **TailwindCSS** (`TWStyles`) dan CSS custom untuk gaya visual seperti:
91
+ - `.font-lora` → Font Lora / Georgia.
92
+ - `.icon` → Ikon Font Awesome dengan ukuran dinamis.
93
+ - `.message-collapse-engage-returners` → Penyorotan teks utama di banner bawah.
94
+
95
+ ---
96
+
97
+ ## 🌐 JSON Endpoint Example
98
+
99
+ ```json
100
+ {
101
+ "paywall": {
102
+ "messageTitleExpand": "Dapatkan akses penuh ke Kompas.id",
103
+ "messageTitleCollapse": "Langganan hanya _offering_price_ per bulan",
104
+ "descriptionExpand": "Nikmati berita premium dan eksklusif hanya di Kompas.id",
105
+ "image": "https://cdn-www.kompas.id/images/paywall-image.jpg"
106
+ },
107
+ "offering": {
108
+ "Q1": {
109
+ "harga_coret": 100000,
110
+ "offering_price": 75000,
111
+ "duration": "per bulan",
112
+ "checkout_url": "https://www.kompas.id/berlangganan?package=Q1",
113
+ "checkout_text": "Langganan Sekarang"
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ ---
120
+
121
+ ## 📊 Event Tracking (GTM)
122
+
123
+ | Event | Description | Data Sent |
124
+ | ------ | ------------ | ---------- |
125
+ | `paywall_viewed` | Dikirim saat komponen dimuat pertama kali. | `paywall_location`, `segment`, `content_id`, `subscription_status`, dll. |
126
+ | `subscribe_button_clicked` | Dikirim saat user klik tombol langganan. | `checkout_url`, `offering_price`, `paywall_position`, dll. |
127
+
128
+ ---
129
+
130
+ ## 🧱 Contoh Penggunaan
131
+
132
+ ```html
133
+ <kompasid-metered-paywall-personalize
134
+ segment="Engaged Returners"
135
+ offering="Q1"
136
+ user_name="Andi"
137
+ countdown-article="2"
138
+ paywall_location="Article Page"
139
+ tracker_content_id="artikel-luar-biasa"
140
+ tracker_page_type="article"
141
+ >
142
+ </kompasid-metered-paywall-personalize>
143
+ ```
144
+
145
+ ---
146
+
147
+ ## 🪄 Catatan
148
+ - Komponen ini bergantung pada file JSON di CDN:
149
+ `https://cdn-www.kompas.id/web-component/metered-paywall-personalize/{segment}.json`
150
+ - Pastikan properti `segment` sesuai dengan file JSON yang tersedia.
151
+ - Jika `segment` tidak ditemukan, komponen tidak akan menampilkan apa pun (`nothing`).
152
+ ---
153
+
154
+ **Dibuat oleh tim Front-End Kompas.id**
155
+ _© Kompas.id, 2025_
@@ -0,0 +1,26 @@
1
+ export interface OfferingItem {
2
+ offering_price: number
3
+ harga_coret: number
4
+ duration?: string
5
+ checkout_url: string
6
+ checkout_text: string
7
+ }
8
+
9
+ export interface OfferingList {
10
+ [key: string]: OfferingItem
11
+ }
12
+
13
+ export interface PaywallData {
14
+ messageTitleExpand: string
15
+ descriptionExpand: string
16
+ messageTitleCollapse: string
17
+ descriptionCollapse?: string
18
+ listBenefits?: string[]
19
+ image: string
20
+ }
21
+
22
+ export interface PackageData {
23
+ segment_name: string
24
+ paywall: PaywallData
25
+ offering: OfferingList
26
+ }