app-tutor-ai-consumer 1.19.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # [1.21.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.20.0...v1.21.0) (2025-07-29)
2
+
3
+ ### Features
4
+
5
+ - add DataHub Config ([745264a](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/745264a166e868884958abb19d0d720e06294ffe))
6
+
7
+ # [1.20.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.19.0...v1.20.0) (2025-07-29)
8
+
9
+ ### Features
10
+
11
+ - add generic error page ([2ac19d0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/2ac19d0d033731508580778dcf679fb9c71ca24f))
12
+
1
13
  # [1.19.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.18.2...v1.19.0) (2025-07-29)
2
14
 
3
15
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-tutor-ai-consumer",
3
- "version": "1.19.0",
3
+ "version": "1.21.0",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "dev": "rspack serve --env=development --config config/rspack/rspack.config.js",
@@ -104,6 +104,7 @@
104
104
  },
105
105
  "dependencies": {
106
106
  "@hot-observability-js/react": "~1.1.0",
107
+ "@hotmart/event-agent-js": "~1.1.2",
107
108
  "@hotmart/sparkie": "~5.1.0",
108
109
  "@optimizely/react-sdk": "~3.2.4",
109
110
  "@tanstack/query-sync-storage-persister": "~5.80.7",
@@ -0,0 +1,27 @@
1
+ <svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <circle opacity="0.5" cx="80" cy="80" r="80" fill="#282C2F"/>
3
+ <g filter="url(#filter0_d_22198_69476)">
4
+ <path d="M32.8 47.9999C32.8 47.1162 33.5164 46.3999 34.4 46.3999H125.6C126.484 46.3999 127.2 47.1162 127.2 47.9999V55.9999H32.8V47.9999Z" fill="#464B52"/>
5
+ <path d="M32.8 56H127.2V112C127.2 112.884 126.484 113.6 125.6 113.6H34.4001C33.5164 113.6 32.8 112.884 32.8 112V56Z" fill="white"/>
6
+ <rect width="11.2" height="3.2" rx="1.6" transform="matrix(1 0 0 -1 74.3999 91.2)" fill="#C9CED4"/>
7
+ <rect width="4" height="4" rx="2" transform="matrix(1 0 0 -1 68 81.6001)" fill="#C9CED4"/>
8
+ <rect width="4" height="4" rx="2" transform="matrix(1 0 0 -1 88 81.6001)" fill="#C9CED4"/>
9
+ </g>
10
+ <rect width="3.2" height="3.2" rx="1.6" transform="matrix(1 0 0 -1 36 52.8)" fill="#E37570"/>
11
+ <rect width="3.2" height="3.2" rx="1.6" transform="matrix(1 0 0 -1 41.6001 52.8)" fill="#EFBA0F"/>
12
+ <rect width="3.2" height="3.2" rx="1.6" transform="matrix(1 0 0 -1 47.2 52.8)" fill="#4ACC82"/>
13
+ <path d="M141.143 46.1714C141.143 53.9981 134.798 60.3429 126.971 60.3429C119.145 60.3429 112.8 53.9981 112.8 46.1714C112.8 38.3447 119.145 32 126.971 32C134.798 32 141.143 38.3447 141.143 46.1714Z" fill="#5981E3"/>
14
+ <path d="M128.297 49.5064H125.566L124.993 38.562H128.87L128.297 49.5064ZM124.947 53.3381C124.947 52.6345 125.138 52.145 125.52 51.8697C125.903 51.579 126.369 51.4337 126.92 51.4337C127.455 51.4337 127.914 51.579 128.297 51.8697C128.679 52.145 128.87 52.6345 128.87 53.3381C128.87 54.0111 128.679 54.5006 128.297 54.8065C127.914 55.0971 127.455 55.2425 126.92 55.2425C126.369 55.2425 125.903 55.0971 125.52 54.8065C125.138 54.5006 124.947 54.0111 124.947 53.3381Z" fill="white"/>
15
+ <defs>
16
+ <filter id="filter0_d_22198_69476" x="24.8" y="44.7999" width="110.4" height="83.2002" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
17
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
18
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
19
+ <feMorphology radius="4.8" operator="erode" in="SourceAlpha" result="effect1_dropShadow_22198_69476"/>
20
+ <feOffset dy="6.4"/>
21
+ <feGaussianBlur stdDeviation="6.4"/>
22
+ <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
23
+ <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_22198_69476"/>
24
+ <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_22198_69476" result="shape"/>
25
+ </filter>
26
+ </defs>
27
+ </svg>
@@ -0,0 +1,27 @@
1
+ <svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <circle opacity="0.5" cx="80" cy="80" r="80" fill="#E6E9ED"/>
3
+ <g filter="url(#filter0_d_22198_92633)">
4
+ <path d="M32.8 47.9999C32.8 47.1162 33.5164 46.3999 34.4 46.3999H125.6C126.484 46.3999 127.2 47.1162 127.2 47.9999V55.9999H32.8V47.9999Z" fill="#464B52"/>
5
+ <path d="M32.8 56H127.2V112C127.2 112.884 126.484 113.6 125.6 113.6H34.4001C33.5164 113.6 32.8 112.884 32.8 112V56Z" fill="white"/>
6
+ <rect width="11.2" height="3.2" rx="1.6" transform="matrix(1 0 0 -1 74.3999 91.2002)" fill="#C9CED4"/>
7
+ <rect width="4" height="4" rx="2" transform="matrix(1 0 0 -1 68 81.6001)" fill="#C9CED4"/>
8
+ <rect width="4" height="4" rx="2" transform="matrix(1 0 0 -1 88 81.6001)" fill="#C9CED4"/>
9
+ </g>
10
+ <rect width="3.2" height="3.2" rx="1.6" transform="matrix(1 0 0 -1 36 52.7998)" fill="#E37570"/>
11
+ <rect width="3.2" height="3.2" rx="1.6" transform="matrix(1 0 0 -1 41.6001 52.7998)" fill="#EFBA0F"/>
12
+ <rect width="3.2" height="3.2" rx="1.6" transform="matrix(1 0 0 -1 47.2 52.7998)" fill="#4ACC82"/>
13
+ <path d="M141.143 46.1714C141.143 53.9981 134.798 60.3429 126.971 60.3429C119.145 60.3429 112.8 53.9981 112.8 46.1714C112.8 38.3447 119.145 32 126.971 32C134.798 32 141.143 38.3447 141.143 46.1714Z" fill="#5981E3"/>
14
+ <path d="M128.297 49.5064H125.566L124.993 38.562H128.87L128.297 49.5064ZM124.947 53.3381C124.947 52.6345 125.138 52.145 125.52 51.8697C125.903 51.579 126.369 51.4337 126.92 51.4337C127.455 51.4337 127.914 51.579 128.297 51.8697C128.679 52.145 128.87 52.6345 128.87 53.3381C128.87 54.0111 128.679 54.5006 128.297 54.8065C127.914 55.0971 127.455 55.2425 126.92 55.2425C126.369 55.2425 125.903 55.0971 125.52 54.8065C125.138 54.5006 124.947 54.0111 124.947 53.3381Z" fill="white"/>
15
+ <defs>
16
+ <filter id="filter0_d_22198_92633" x="24.8" y="44.7999" width="110.4" height="83.2002" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
17
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
18
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
19
+ <feMorphology radius="4.8" operator="erode" in="SourceAlpha" result="effect1_dropShadow_22198_92633"/>
20
+ <feOffset dy="6.4"/>
21
+ <feGaussianBlur stdDeviation="6.4"/>
22
+ <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
23
+ <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_22198_92633"/>
24
+ <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_22198_92633" result="shape"/>
25
+ </filter>
26
+ </defs>
27
+ </svg>
@@ -0,0 +1,10 @@
1
+ export const DataHubActions = {
2
+ OPEN: 'open',
3
+ TRY: 'try',
4
+ CLICK: 'click',
5
+ CLOSE: 'close',
6
+ SELECT: 'select',
7
+ VIEW: 'view'
8
+ } as const
9
+
10
+ export type DataHubActionTypes = (typeof DataHubActions)[keyof typeof DataHubActions]
@@ -0,0 +1,43 @@
1
+ export const DATA_VERSION = '1.0'
2
+
3
+ export const EVENT_VERSION = '1.1'
4
+
5
+ export const System = {
6
+ HOTMART_CLUB: 'hotmart_club'
7
+ } as const
8
+
9
+ export const ActionNames = {
10
+ CLICK_HOTMART_TUTOR: 'click_hotmart_tutor'
11
+ } as const
12
+
13
+ export const ScreenNames = {
14
+ NOT_APP_EVENT: 'NOT_APP_EVENT',
15
+ HOME_CONSUMER: 'HOME_CONSUMER'
16
+ } as const
17
+
18
+ export const Platform = {
19
+ WEB: 'WEB'
20
+ } as const
21
+
22
+ export const ResultType = {
23
+ SUCCESS: 'SUCCESSFUL',
24
+ FAILURE: 'FAILURE',
25
+ FAILURE_DESCRIPTION: 'NOT_FAILURE_RESULT_EVENT'
26
+ } as const
27
+
28
+ export const ComponentNames = {
29
+ BUTTON_LIKE_ANSWER: 'BUTTON_LIKE_ANSWER',
30
+ BUTTON_DISLIKE_ANSWER: 'BUTTON_DISLIKE_ANSWER'
31
+ } as const
32
+
33
+ export const ComponentSource = {
34
+ ACTION_BAR_STATUS: 'ACTION_BAR_STATUS'
35
+ } as const
36
+
37
+ export const ArrivedFrom = {
38
+ HOME: 'HOME'
39
+ } as const
40
+
41
+ export const PushKind = {
42
+ NOT_PUSH_EVENT: 'NOT_PUSH_EVENT'
43
+ } as const
@@ -0,0 +1,4 @@
1
+ export const DataHubEntities = {
2
+ ADMIN: 'admin',
3
+ HOME: 'home'
4
+ } as const
@@ -0,0 +1,5 @@
1
+ export * from './constants'
2
+ export * from './entities'
3
+ export { default as DataHubService } from './service'
4
+ export { default as DataHubStore } from './store'
5
+ export * from './types'
@@ -0,0 +1,72 @@
1
+ import { APP_VERSION } from '@/src/lib/utils'
2
+ import { DataHubStore } from '..'
3
+ import { Platform } from '../constants'
4
+ import type {
5
+ ActionNamesType,
6
+ DataHubEntityTypes,
7
+ InitDataParams,
8
+ PlatformType,
9
+ SchemaType
10
+ } from '../types'
11
+
12
+ import { ClubVersion, UserRole } from './constants'
13
+ import type { ClubVersionType, UserRoleType } from './types'
14
+
15
+ abstract class BaseSchema implements SchemaType, InitDataParams {
16
+ appVersion: string
17
+ osVersion: string
18
+ platform: PlatformType
19
+ platformDetail: string
20
+
21
+ membershipId: string
22
+ membershipSlug: string
23
+ sessionId: string
24
+ ucode: string
25
+ userId: number
26
+
27
+ clubVersion: ClubVersionType
28
+ userRole: UserRoleType
29
+ url: string
30
+
31
+ abstract action: ActionNamesType
32
+ abstract entity: DataHubEntityTypes
33
+ abstract isLogged: boolean
34
+ abstract getDataHubEventData(): Record<string, unknown>
35
+
36
+ constructor() {
37
+ this.appVersion = APP_VERSION
38
+ this.clubVersion = ClubVersion.MEMBERSHIP
39
+ this.membershipId = DataHubStore.membershipId
40
+ this.membershipSlug = DataHubStore.membershipSlug
41
+ this.osVersion = DataHubStore.browserInfo.version ?? ''
42
+ this.platform = Platform.WEB
43
+ this.platformDetail = DataHubStore.browserInfo.name ?? ''
44
+ this.sessionId = DataHubStore.sessionId
45
+ this.ucode = DataHubStore.ucode
46
+ this.url = window.location.href
47
+ this.userId = DataHubStore.userId
48
+ this.userRole = UserRole.CONSUMER
49
+ }
50
+
51
+ prepare(): Record<string, unknown> {
52
+ return {
53
+ action: this.action,
54
+ entity: this.entity,
55
+ isLogged: this.isLogged,
56
+ appVersion: this.appVersion,
57
+ osVersion: this.osVersion,
58
+ platform: this.platform,
59
+ platformDetail: this.platformDetail,
60
+ membershipId: this.membershipId,
61
+ membershipSlug: this.membershipSlug,
62
+ sessionId: this.sessionId,
63
+ ucode: this.ucode,
64
+ userId: this.userId,
65
+ clubVersion: this.clubVersion,
66
+ userRole: this.userRole,
67
+ url: this.url
68
+ }
69
+ }
70
+ }
71
+
72
+ export default BaseSchema
@@ -0,0 +1,10 @@
1
+ export const ClubVersion = {
2
+ MEMBERSHIP: 'MEMBERSHIP'
3
+ } as const
4
+
5
+ export const UserRole = {
6
+ OWNER: 'OWNER',
7
+ MODERATOR: 'MODERATOR',
8
+ ADMIN: 'ADMIN',
9
+ CONSUMER: 'CONSUMER'
10
+ } as const
@@ -0,0 +1,2 @@
1
+ export * from './constants'
2
+ export * from './types'
@@ -0,0 +1,81 @@
1
+ import { chance } from '@/src/config/tests'
2
+ import { DataHubStore } from '../../..'
3
+ import { ActionNames, ComponentNames, ScreenNames } from '../../../constants'
4
+ import { DataHubEntities } from '../../../entities'
5
+ import { UserRole } from '../../constants'
6
+ import ClickHotmartTutor from '../click-hotmart-tutor'
7
+ import { ButtonReactions } from '../constants'
8
+
9
+ describe('ClickHotmartTutor', () => {
10
+ it('should send the event with the correct structure', () => {
11
+ const event = {
12
+ reactionType: ButtonReactions.DISLIKE,
13
+ messageId: chance.guid(),
14
+ hasAccess: true,
15
+ isLogged: true
16
+ }
17
+
18
+ expect(new ClickHotmartTutor(event).getDataHubEventData()).toMatchObject({
19
+ action: ActionNames.CLICK_HOTMART_TUTOR,
20
+ appVersion: '',
21
+ clubVersion: 'MEMBERSHIP',
22
+ componentName: ComponentNames.BUTTON_DISLIKE_ANSWER,
23
+ entity: DataHubEntities.HOME,
24
+ hasAccess: event.hasAccess,
25
+ isLogged: event.isLogged,
26
+ membershipId: 'UNDEFINED',
27
+ membershipSlug: 'UNDEFINED',
28
+ messageId: event.messageId,
29
+ osVersion: '537.36',
30
+ platform: 'WEB',
31
+ platformDetail: 'WebKit',
32
+ screenName: ScreenNames.HOME_CONSUMER,
33
+ sessionId: 'UNDEFINED',
34
+ ucode: 'UNDEFINED',
35
+ url: 'http://localhost:3000/',
36
+ userId: 0,
37
+ userRole: UserRole.CONSUMER
38
+ })
39
+ })
40
+
41
+ it('should send reflect DataHubStore current state when sending the event', () => {
42
+ const event = {
43
+ reactionType: ButtonReactions.LIKE,
44
+ messageId: chance.guid(),
45
+ hasAccess: true,
46
+ isLogged: true
47
+ }
48
+
49
+ const store = {
50
+ ucode: chance.guid(),
51
+ membershipId: chance.guid(),
52
+ membershipSlug: chance.animal(),
53
+ sessionId: chance.guid(),
54
+ userId: chance.integer()
55
+ }
56
+
57
+ DataHubStore.initData(store)
58
+
59
+ expect(new ClickHotmartTutor(event).getDataHubEventData()).toMatchObject({
60
+ action: ActionNames.CLICK_HOTMART_TUTOR,
61
+ appVersion: '',
62
+ clubVersion: 'MEMBERSHIP',
63
+ componentName: ComponentNames.BUTTON_LIKE_ANSWER,
64
+ entity: DataHubEntities.HOME,
65
+ hasAccess: event.hasAccess,
66
+ isLogged: event.isLogged,
67
+ membershipId: store.membershipId,
68
+ membershipSlug: store.membershipSlug,
69
+ messageId: event.messageId,
70
+ osVersion: '537.36',
71
+ platform: 'WEB',
72
+ platformDetail: 'WebKit',
73
+ screenName: ScreenNames.HOME_CONSUMER,
74
+ sessionId: store.sessionId,
75
+ ucode: store.ucode,
76
+ url: 'http://localhost:3000/',
77
+ userId: store.userId,
78
+ userRole: UserRole.CONSUMER
79
+ })
80
+ })
81
+ })
@@ -0,0 +1,61 @@
1
+ import { ActionNames, ComponentNames, ScreenNames } from '../../constants'
2
+ import { DataHubEntities } from '../../entities'
3
+ import type {
4
+ ActionNamesType,
5
+ ComponentNamesType,
6
+ DataHubEntityTypes,
7
+ ScreenNamesType
8
+ } from '../../types'
9
+ import BaseSchema from '../base-schema'
10
+
11
+ import { ButtonReactions } from './constants'
12
+ import type { ButtonReactionsType } from './types'
13
+
14
+ class ClickHotmartTutor extends BaseSchema {
15
+ private componentName: ComponentNamesType
16
+ private screenName: ScreenNamesType
17
+ private messageId: string
18
+ private hasAccess: boolean
19
+
20
+ action: ActionNamesType
21
+ entity: DataHubEntityTypes
22
+ isLogged: boolean
23
+
24
+ constructor({
25
+ reactionType,
26
+ messageId,
27
+ hasAccess = true,
28
+ isLogged = true
29
+ }: {
30
+ reactionType: ButtonReactionsType
31
+ messageId: string
32
+ hasAccess?: boolean
33
+ isLogged?: boolean
34
+ }) {
35
+ super()
36
+
37
+ this.action = ActionNames.CLICK_HOTMART_TUTOR
38
+ this.entity = DataHubEntities.HOME
39
+
40
+ this.componentName =
41
+ reactionType === ButtonReactions.LIKE
42
+ ? ComponentNames.BUTTON_LIKE_ANSWER
43
+ : ComponentNames.BUTTON_DISLIKE_ANSWER
44
+ this.screenName = ScreenNames.HOME_CONSUMER
45
+ this.messageId = messageId
46
+ this.hasAccess = hasAccess
47
+ this.isLogged = isLogged
48
+ }
49
+
50
+ getDataHubEventData(): Record<string, unknown> {
51
+ return {
52
+ ...super.prepare(),
53
+ componentName: this.componentName,
54
+ screenName: this.screenName,
55
+ messageId: this.messageId,
56
+ hasAccess: this.hasAccess
57
+ }
58
+ }
59
+ }
60
+
61
+ export default ClickHotmartTutor
@@ -0,0 +1,4 @@
1
+ export const ButtonReactions = {
2
+ LIKE: 'LIKE',
3
+ DISLIKE: 'DISLIKE'
4
+ }
@@ -0,0 +1,3 @@
1
+ export { default as ClickHotmartTutor } from './click-hotmart-tutor'
2
+ export * from './constants'
3
+ export * from './types'
@@ -0,0 +1,3 @@
1
+ import type { ButtonReactions } from './constants'
2
+
3
+ export type ButtonReactionsType = (typeof ButtonReactions)[keyof typeof ButtonReactions]
@@ -0,0 +1,19 @@
1
+ import type { ClubVersion, UserRole } from './constants'
2
+
3
+ export type UserRoleType = (typeof UserRole)[keyof typeof UserRole]
4
+
5
+ export type ClubVersionType = (typeof ClubVersion)[keyof typeof ClubVersion]
6
+
7
+ export type SchemaBasedProps = {
8
+ membershipSlug?: string
9
+ membershipId?: string
10
+ ucode?: string
11
+ userId?: number
12
+ hasAccess?: boolean
13
+ }
14
+
15
+ export type BaseSchemaProps = {
16
+ membershipId: string
17
+ membershipSlug: string
18
+ userId: string
19
+ }
@@ -0,0 +1,32 @@
1
+ import EventAgent from '@hotmart/event-agent-js'
2
+
3
+ import { productionMode } from '@/src/lib/utils'
4
+
5
+ import { DATA_VERSION, EVENT_VERSION, System } from './constants'
6
+ import type { SendEventParams } from './types'
7
+
8
+ EventAgent.mode = productionMode ? 'production' : 'staging'
9
+
10
+ class DataHubService {
11
+ sendEvent({
12
+ schema,
13
+ dataVersion = DATA_VERSION,
14
+ eventVersion = EVENT_VERSION,
15
+ system = System.HOTMART_CLUB
16
+ }: SendEventParams): void {
17
+ const dataHubEvent = {
18
+ system,
19
+ action: schema.action,
20
+ entity: schema.entity,
21
+ data_version: dataVersion,
22
+ event_version: eventVersion,
23
+ event: schema.getDataHubEventData()
24
+ }
25
+
26
+ if (!productionMode) console.warn('DataHub event dispatched: ', dataHubEvent)
27
+
28
+ void EventAgent.send(dataHubEvent)
29
+ }
30
+ }
31
+
32
+ export default new DataHubService()
@@ -0,0 +1,87 @@
1
+ import { UAParser } from 'ua-parser-js'
2
+
3
+ import type { InitDataParams } from './types'
4
+
5
+ class DataHubStore {
6
+ private instanceObj?: {
7
+ userAgentParser: UAParser.IResult
8
+ browserInfo: UAParser.IBrowser
9
+ deviceType: string
10
+ deviceVendor: UAParser.IDevice['vendor']
11
+ deviceModel: UAParser.IDevice['model']
12
+ ucode: string
13
+ sessionId: string
14
+ userId: number
15
+ membershipSlug: string
16
+ membershipId: string
17
+ }
18
+
19
+ private get instance() {
20
+ return this.instanceObj ?? this.resetData()
21
+ }
22
+
23
+ initData({ ucode, membershipId, membershipSlug, sessionId, userId }: InitDataParams) {
24
+ this.instance.userAgentParser = UAParser(navigator.userAgent)
25
+ this.instance.browserInfo = this.instance.userAgentParser.browser
26
+ this.instance.deviceType =
27
+ this.instance.userAgentParser.device?.type?.toUpperCase() || 'DESKTOP'
28
+ this.instance.deviceVendor = this.instance.userAgentParser.device?.vendor?.toUpperCase() ?? ''
29
+ this.instance.deviceModel = this.instance.userAgentParser.device?.model?.toUpperCase() ?? ''
30
+ this.instance.ucode = ucode
31
+ this.instance.sessionId = sessionId
32
+ this.instance.userId = userId || 0
33
+ this.instance.membershipSlug = membershipSlug
34
+ this.instance.membershipId = membershipId
35
+ }
36
+
37
+ resetData() {
38
+ const userAgent = UAParser(navigator.userAgent)
39
+
40
+ const clearData = {
41
+ userAgentParser: userAgent,
42
+ browserInfo: userAgent.browser,
43
+ deviceType: userAgent.device.type?.toUpperCase() || 'DESKTOP',
44
+ deviceVendor: userAgent.device.vendor,
45
+ deviceModel: userAgent.device.model,
46
+ ucode: 'UNDEFINED',
47
+ sessionId: 'UNDEFINED',
48
+ userId: 0,
49
+ membershipSlug: 'UNDEFINED',
50
+ membershipId: 'UNDEFINED'
51
+ }
52
+
53
+ this.instanceObj = clearData
54
+
55
+ return clearData
56
+ }
57
+
58
+ get userAgentParser() {
59
+ return this.instance.userAgentParser
60
+ }
61
+
62
+ get browserInfo() {
63
+ return this.instance.browserInfo
64
+ }
65
+
66
+ get ucode() {
67
+ return this.instance.ucode
68
+ }
69
+
70
+ get sessionId() {
71
+ return this.instance.sessionId
72
+ }
73
+
74
+ get userId() {
75
+ return this.instance.userId
76
+ }
77
+
78
+ get membershipSlug() {
79
+ return this.instance.membershipSlug
80
+ }
81
+
82
+ get membershipId() {
83
+ return this.instance.membershipId
84
+ }
85
+ }
86
+
87
+ export default new DataHubStore()
@@ -0,0 +1,35 @@
1
+ import type { ActionNames, ComponentNames, Platform, ScreenNames, System } from './constants'
2
+ import type { DataHubEntities } from './entities'
3
+
4
+ export type ActionNamesType = (typeof ActionNames)[keyof typeof ActionNames]
5
+ export type DataHubEntityTypes = (typeof DataHubEntities)[keyof typeof DataHubEntities]
6
+ export type PlatformType = (typeof Platform)[keyof typeof Platform]
7
+ export type SystemType = (typeof System)[keyof typeof System]
8
+ export type ComponentNamesType = (typeof ComponentNames)[keyof typeof ComponentNames]
9
+ export type ScreenNamesType = (typeof ScreenNames)[keyof typeof ScreenNames]
10
+
11
+ export type SchemaType = {
12
+ action: ActionNamesType
13
+ entity: DataHubEntityTypes
14
+ osVersion: string
15
+ appVersion: string
16
+ platformDetail: string
17
+ platform: PlatformType
18
+ prepare: () => Record<string, unknown>
19
+ getDataHubEventData: () => Record<string, unknown>
20
+ }
21
+
22
+ export type SendEventParams = {
23
+ schema: SchemaType
24
+ system?: SystemType
25
+ dataVersion?: string
26
+ eventVersion?: string
27
+ }
28
+
29
+ export type InitDataParams = {
30
+ ucode: string
31
+ sessionId: string
32
+ userId: number
33
+ membershipSlug: string
34
+ membershipId: string
35
+ }
@@ -35,6 +35,7 @@ if (devMode) {
35
35
  namespace: 'tutor_v1-2',
36
36
  author: 'user',
37
37
  clubName: 'comofazerumvideodeteste',
38
+ membershipSlug: 'comofazerumvideodeteste',
38
39
  productName: 'Curso de Assinatura',
39
40
  productId: 4266504,
40
41
  sessionId: v4(),
@@ -1,12 +1,67 @@
1
+ import clsx from 'clsx'
1
2
  import { useTranslation } from 'react-i18next'
2
3
 
4
+ import ErrorDarkSVG from '@/public/assets/svg/error-dark.svg?url'
5
+ import ErrorLightSVG from '@/public/assets/svg/error-light.svg?url'
6
+ import { Button } from '@/src/lib/components'
7
+ import { PageLayout, TutorWidgetEvents, useWidgetSettingsAtom } from '@/src/modules/widget'
8
+ import { WidgetHeader } from '@/src/modules/widget/components/header'
9
+
3
10
  function GenericError() {
4
11
  const { t } = useTranslation()
12
+ const [settings] = useWidgetSettingsAtom()
13
+ const isDarkMode = settings?.config?.theme === 'dark'
5
14
 
6
15
  return (
7
- <div>
8
- <h4 className='text-xl font-bold'>{t('generic.error')}</h4>
9
- </div>
16
+ <PageLayout className='p-5'>
17
+ <WidgetHeader enabledButtons={['close']} showContent={false} />
18
+
19
+ <div className='flex h-full flex-col items-center justify-center p-10'>
20
+ <div className='mb-8 flex flex-col items-center gap-1 text-center'>
21
+ <img
22
+ alt={t('generic_error.image_alt')}
23
+ className='mb-4'
24
+ src={isDarkMode ? ErrorDarkSVG : ErrorLightSVG}
25
+ width={200}
26
+ height={200}
27
+ />
28
+
29
+ <h2
30
+ className={clsx('text-xl font-bold', {
31
+ 'text-white': isDarkMode,
32
+ 'text-gray-800': !isDarkMode
33
+ })}>
34
+ {t('generic_error.title')}
35
+ </h2>
36
+
37
+ <p
38
+ className={clsx('text-sm', {
39
+ 'text-gray-400': isDarkMode,
40
+ 'text-gray-500': !isDarkMode
41
+ })}>
42
+ {t('generic_error.description')}
43
+ </p>
44
+ </div>
45
+
46
+ <div className='flex w-full flex-col gap-4'>
47
+ <Button
48
+ variant='brand'
49
+ className='w-full rounded-lg py-2'
50
+ onClick={() => window.location.reload()}
51
+ aria-label='Retry Button'>
52
+ <span className='font-light'>{t('general.buttons.try_again')}</span>
53
+ </Button>
54
+
55
+ <Button
56
+ variant='secondary'
57
+ className='w-full rounded-lg py-2'
58
+ onClick={() => TutorWidgetEvents['c3po-app-widget-hide'].dispatch()}
59
+ aria-label='Close Button'>
60
+ <span className='font-light'>{t('general.buttons.close')}</span>
61
+ </Button>
62
+ </div>
63
+ </div>
64
+ </PageLayout>
10
65
  )
11
66
  }
12
67
 
@@ -0,0 +1,53 @@
1
+ import { DataHubService } from '@/src/config/datahub'
2
+ import { ButtonReactions, ClickHotmartTutor } from '@/src/config/datahub/schemas/tutor'
3
+ import { render, screen } from '@/src/config/tests'
4
+ import ParsedMessageBuilder from '../../__tests__/parsed-message.builder'
5
+
6
+ import MessageActions from './message-actions'
7
+
8
+ describe('<MessageActions/>', () => {
9
+ const defaultProps = {
10
+ message: new ParsedMessageBuilder()
11
+ }
12
+
13
+ const renderComponent = (props = defaultProps) => render(<MessageActions {...props} />)
14
+
15
+ it('should render without errors', () => {
16
+ renderComponent()
17
+
18
+ expect(screen.getByRole('button', { name: /general.buttons.like/i })).toBeInTheDocument()
19
+ expect(screen.getByRole('button', { name: /general.buttons.dislike/i })).toBeInTheDocument()
20
+ expect(screen.getByRole('button', { name: /general.buttons.copy/i })).toBeInTheDocument()
21
+ })
22
+
23
+ it('should copy the current message text when clicking the copy button', async () => {
24
+ vi.spyOn(navigator.clipboard, 'writeText').mockImplementation(() => Promise.resolve(undefined))
25
+
26
+ const { user } = renderComponent()
27
+
28
+ const copyBtn = screen.getByRole('button', { name: /general.buttons.copy/i })
29
+
30
+ await user.click(copyBtn)
31
+
32
+ expect(navigator.clipboard.writeText).toBeCalledTimes(1)
33
+ expect(navigator.clipboard.writeText).toHaveBeenNthCalledWith(1, defaultProps.message.text)
34
+ })
35
+
36
+ it('should send like event when clicking the like button', async () => {
37
+ vi.spyOn(DataHubService, 'sendEvent').mockImplementation(() => undefined)
38
+
39
+ const { user } = renderComponent()
40
+
41
+ const likeBtn = screen.getByRole('button', { name: /general.buttons.like/i })
42
+
43
+ await user.click(likeBtn)
44
+
45
+ expect(DataHubService.sendEvent).toBeCalledTimes(1)
46
+ expect(DataHubService.sendEvent).toHaveBeenNthCalledWith(1, {
47
+ schema: new ClickHotmartTutor({
48
+ messageId: defaultProps.message.id,
49
+ reactionType: ButtonReactions.LIKE
50
+ })
51
+ })
52
+ })
53
+ })
@@ -3,6 +3,9 @@ import dayjs from 'dayjs'
3
3
  import type { CSSProperties } from 'react'
4
4
  import { useTranslation } from 'react-i18next'
5
5
 
6
+ import { DataHubService } from '@/src/config/datahub'
7
+ import type { ButtonReactionsType } from '@/src/config/datahub/schemas/tutor'
8
+ import { ButtonReactions, ClickHotmartTutor } from '@/src/config/datahub/schemas/tutor'
6
9
  import { Button, Icon } from '@/src/lib/components'
7
10
  import type { ParsedMessage } from '../../types'
8
11
 
@@ -14,7 +17,8 @@ export type MessageActionsProps = {
14
17
 
15
18
  function MessageActions({ message, className, showActions = false }: MessageActionsProps) {
16
19
  const { t } = useTranslation()
17
- const copyToClipboard = () => {
20
+
21
+ const copyToClipboard = (): void => {
18
22
  if (!message.text) return
19
23
 
20
24
  navigator.clipboard.writeText(message.text).catch((err) => {
@@ -22,6 +26,11 @@ function MessageActions({ message, className, showActions = false }: MessageActi
22
26
  })
23
27
  }
24
28
 
29
+ const handleReaction = (reactionType: ButtonReactionsType = ButtonReactions.LIKE): void => {
30
+ const schema = new ClickHotmartTutor({ messageId: message.id, reactionType })
31
+ return DataHubService.sendEvent({ schema })
32
+ }
33
+
25
34
  return (
26
35
  <div className={className}>
27
36
  <span className='text-xs tracking-wide'>
@@ -32,10 +41,13 @@ function MessageActions({ message, className, showActions = false }: MessageActi
32
41
  className={clsx('flex flex-nowrap gap-2 text-neutral-600', {
33
42
  hidden: !showActions
34
43
  })}>
35
- <Button aria-label={t('general.buttons.like')}>
44
+ <Button onClick={() => handleReaction()} aria-label={t('general.buttons.like')}>
36
45
  <Icon name='like' className='h-3 w-3.5' />
37
46
  </Button>
38
- <Button className='rotate-180 scale-x-[-1]' aria-label={t('general.buttons.dislike')}>
47
+ <Button
48
+ className='rotate-180 scale-x-[-1]'
49
+ onClick={() => handleReaction(ButtonReactions.DISLIKE)}
50
+ aria-label={t('general.buttons.dislike')}>
39
51
  <Icon name='like' className='h-3 w-3.5' />
40
52
  </Button>
41
53
  <Button onClick={copyToClipboard} aria-label={t('general.buttons.copy')}>
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useState } from 'react'
2
2
 
3
+ import { DataHubStore } from '@/src/config/datahub'
3
4
  import { initDayjs } from '@/src/config/dayjs'
4
5
  import { initAxios } from '@/src/config/request/api'
5
6
  import { SparkieService } from '@/src/modules/sparkie'
@@ -10,6 +11,13 @@ const init = async (settings: WidgetSettingProps) => {
10
11
  try {
11
12
  initAxios(settings.hotmartToken)
12
13
  await initDayjs(settings.locale)
14
+ DataHubStore.initData({
15
+ ucode: settings.user?.ucode ?? '',
16
+ membershipId: settings.membershipId ?? '',
17
+ membershipSlug: settings.membershipSlug ?? '',
18
+ sessionId: settings.sessionId,
19
+ userId: Number(settings.userId)
20
+ })
13
21
  await SparkieService.initSparkie({
14
22
  token: settings?.hotmartToken,
15
23
  skipPresenceSetup: true,