@streamlayer/feature-gamification 0.21.2 → 0.23.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.
@@ -6,23 +6,9 @@ import type { PlainMessage } from '@bufbuild/protobuf';
6
6
  import { WritableAtom } from 'nanostores';
7
7
  import * as queries from './queries';
8
8
  import { leaderboard } from './leaderboard';
9
+ import { OnboardingStatus } from './onboarding';
9
10
  import { LeaderboardItem } from './queries/leaderboard';
10
11
  import { GamificationBackground } from './';
11
- /**
12
- * Required: in-app should be displayed and questions not available
13
- * Optional: in-app should be displayed but questions are available
14
- * Completed: user completed onboarding, cached in browser. Linked by eventId, organizationId and userId
15
- * Disabled: no in-app but questions are available
16
- * Unavailable: no in-app and questions not available [behavior is discussed]
17
- */
18
- export declare enum OnboardingStatus {
19
- Unset = "unset",
20
- Required = "required",
21
- Optional = "optional",
22
- Completed = "completed",
23
- Disabled = "disabled",
24
- Unavailable = "unavailable"
25
- }
26
12
  /**
27
13
  * Gamification (Games) Overlay
28
14
  * Includes:
@@ -42,29 +28,25 @@ export declare class Gamification extends AbstractFeature<'games', PlainMessage<
42
28
  /** leaderboard list */
43
29
  leaderboardList: ReturnType<typeof leaderboard>;
44
30
  /** onboarding status */
45
- onboardingStatus: WritableAtom<OnboardingStatus | undefined>;
31
+ onboardingStatus: {
32
+ $store: WritableAtom<OnboardingStatus | undefined>;
33
+ submitInplay: () => Promise<void>;
34
+ };
46
35
  /** opened question */
47
36
  openedQuestion: GamificationBackground['openedQuestion'];
48
37
  /** pinned leaderboard id */
49
38
  openedUser: WritableAtom<LeaderboardItem | undefined>;
39
+ closeFeature: () => void;
40
+ openFeature: () => void;
50
41
  private notifications;
51
42
  private transport;
52
- private closeFeature;
53
- private openFeature;
54
43
  /** gamification background class, handle subscriptions and notifications for closed overlay */
55
44
  private background;
56
45
  /** Browser cache */
57
46
  private storage;
58
47
  constructor(config: FeatureProps, source: FeatureSource, instance: StreamLayerContext);
59
- /**
60
- * check onboarding status, sync with browser cache
61
- * retrieve onboarding settings from api
62
- */
63
- onboardingProcess: () => Promise<void>;
64
- showOnboardingInApp: () => void;
65
48
  connect: (transport: StreamLayerContext['transport']) => void;
66
49
  disconnect: () => void;
67
- submitInplay: () => Promise<void>;
68
50
  submitAnswer: (questionId: string, answerId: string) => Promise<void>;
69
51
  skipQuestion: (questionId: string) => Promise<void>;
70
52
  openQuestion: (questionId: string) => void;
@@ -6,24 +6,9 @@ import * as queries from './queries';
6
6
  import * as actions from './queries/actions';
7
7
  import { GamificationStorage } from './storage';
8
8
  import { leaderboard } from './leaderboard';
9
+ import { onboarding } from './onboarding';
9
10
  import { gamificationBackground } from './';
10
11
  const GamificationQuestionTypes = new Set([QuestionType.POLL, QuestionType.PREDICTION, QuestionType.TRIVIA]);
11
- /**
12
- * Required: in-app should be displayed and questions not available
13
- * Optional: in-app should be displayed but questions are available
14
- * Completed: user completed onboarding, cached in browser. Linked by eventId, organizationId and userId
15
- * Disabled: no in-app but questions are available
16
- * Unavailable: no in-app and questions not available [behavior is discussed]
17
- */
18
- export var OnboardingStatus;
19
- (function (OnboardingStatus) {
20
- OnboardingStatus["Unset"] = "unset";
21
- OnboardingStatus["Required"] = "required";
22
- OnboardingStatus["Optional"] = "optional";
23
- OnboardingStatus["Completed"] = "completed";
24
- OnboardingStatus["Disabled"] = "disabled";
25
- OnboardingStatus["Unavailable"] = "unavailable";
26
- })(OnboardingStatus || (OnboardingStatus = {}));
27
12
  /**
28
13
  * Gamification (Games) Overlay
29
14
  * Includes:
@@ -48,10 +33,10 @@ export class Gamification extends AbstractFeature {
48
33
  openedQuestion;
49
34
  /** pinned leaderboard id */
50
35
  openedUser;
51
- notifications;
52
- transport;
53
36
  closeFeature;
54
37
  openFeature;
38
+ notifications;
39
+ transport;
55
40
  /** gamification background class, handle subscriptions and notifications for closed overlay */
56
41
  background;
57
42
  /** Browser cache */
@@ -64,131 +49,76 @@ export class Gamification extends AbstractFeature {
64
49
  this.feedList = this.background.feedList;
65
50
  this.openedUser = createSingleStore(undefined);
66
51
  this.leaderboardId = new SingleStore(createSingleStore(this.settings.getValue('pinnedLeaderboardId')), 'pinnedLeaderboardId').getStore();
67
- this.onboardingStatus = new SingleStore(createSingleStore(OnboardingStatus.Unset), 'onboardingStatus').getStore();
52
+ this.onboardingStatus = onboarding(this, this.background, instance.transport, instance.notifications);
68
53
  this.notifications = instance.notifications;
69
54
  this.transport = instance.transport;
70
55
  this.closeFeature = instance.sdk.closeFeature;
71
56
  this.openFeature = () => instance.sdk.openFeature(FeatureType.GAMES);
72
57
  this.openedQuestion = this.background.openedQuestion;
73
58
  this.leaderboardList = leaderboard(this.transport, this.background.slStreamId);
74
- this.onboardingStatus.subscribe((onboardingStatus) => {
75
- if (onboardingStatus === OnboardingStatus.Optional || OnboardingStatus.Required) {
76
- this.showOnboardingInApp();
77
- }
78
- });
79
59
  this.status.subscribe((status) => {
80
60
  if (status === FeatureStatus.Ready) {
81
- this.notifications.close(this.background.getCurrentSessionId({ prefix: 'onboarding' }));
82
61
  this.connect(instance.transport);
83
62
  }
84
63
  else {
85
64
  this.disconnect();
86
65
  }
87
66
  });
88
- this.onboardingStatus.subscribe((onboardingStatus) => {
89
- if (onboardingStatus) {
90
- this.background.activeQuestionId.invalidate();
91
- }
92
- });
67
+ /**
68
+ * listen for active question and show in-app notification
69
+ */
93
70
  this.background.activeQuestionId.listen((question) => {
94
- if (question && question.data && this.onboardingStatus.get()) {
71
+ if (question && question.data && this.onboardingStatus.$store.get()) {
95
72
  if (question.data.question?.id !== undefined &&
96
73
  question.data.question.notification !== undefined &&
97
74
  question.data.moderation?.bypassNotifications?.inAppSilence !== SilenceSetting.ON &&
98
- GamificationQuestionTypes.has(question.data.question.type) &&
99
75
  question.data.question.status === QuestionStatus.ACTIVE) {
100
- this.notifications.add({
101
- type: NotificationType.QUESTION,
102
- action: () => question.data?.question && this.openQuestion(question.data.question.id),
103
- close: () => question.data?.question && this.closeQuestion(question.data.question.id),
104
- autoHideDuration: +(question.data.moderation?.question?.appearance?.autoHideInterval || '1000'),
105
- id: this.background.getCurrentSessionId({ prefix: 'notification', entity: question.data.question.id }),
106
- data: {
107
- title: question.data.question.notification.title,
108
- color: question.data.question.notification.indicatorColor,
109
- icon: question.data.question.notification.image,
110
- imageMode: question.data.question.notification.imageMode,
111
- imagePosition: question.data.question.notification.imagePosition,
112
- },
113
- });
76
+ if (GamificationQuestionTypes.has(question.data.question.type)) {
77
+ this.notifications.add({
78
+ type: NotificationType.QUESTION,
79
+ action: () => question.data?.question && this.openQuestion(question.data.question.id),
80
+ close: () => question.data?.question && this.closeQuestion(question.data.question.id),
81
+ autoHideDuration: 1000 * +(question.data.question?.appearance?.autoHideInterval || '5'),
82
+ id: this.background.getCurrentSessionId({ prefix: 'notification', entity: question.data.question.id }),
83
+ data: {
84
+ questionType: question.data.question.type,
85
+ question: {
86
+ title: question.data.question.notification.title,
87
+ },
88
+ },
89
+ });
90
+ }
91
+ else if (question.data.question.type === QuestionType.FACTOID) {
92
+ const instantView = {
93
+ heading: '',
94
+ body: '',
95
+ image: '',
96
+ video: {
97
+ id: '',
98
+ url: '',
99
+ thumbnailUrl: '',
100
+ },
101
+ webLink: {
102
+ label: '',
103
+ url: '',
104
+ },
105
+ };
106
+ this.notifications.add({
107
+ type: NotificationType.QUESTION,
108
+ action: () => question.data?.question && this.openQuestion(question.data.question.id),
109
+ close: () => question.data?.question && this.closeQuestion(question.data.question.id),
110
+ autoHideDuration: 1000 * +(question.data.question?.appearance?.autoHideInterval || '5'),
111
+ id: this.background.getCurrentSessionId({ prefix: 'notification', entity: question.data.question.id }),
112
+ data: {
113
+ questionType: question.data.question.type,
114
+ insight: instantView,
115
+ },
116
+ });
117
+ }
114
118
  }
115
119
  }
116
120
  });
117
- void this.onboardingProcess();
118
- this.background.userId.subscribe((userId) => {
119
- if (userId) {
120
- void this.onboardingProcess();
121
- }
122
- });
123
- this.background.moderation.subscribe((value) => {
124
- if (value.data) {
125
- void this.onboardingProcess();
126
- }
127
- });
128
121
  }
129
- /**
130
- * check onboarding status, sync with browser cache
131
- * retrieve onboarding settings from api
132
- */
133
- onboardingProcess = async () => {
134
- const userId = this.background.userId.get();
135
- if (!userId) {
136
- return;
137
- }
138
- const onboardingStatus = this.storage.getOnboardingStatus({
139
- userId,
140
- organizationId: this.background.organizationId.get() || '',
141
- eventId: this.background.slStreamId.get() || '',
142
- });
143
- if (onboardingStatus === OnboardingStatus.Completed) {
144
- this.onboardingStatus.set(OnboardingStatus.Completed);
145
- }
146
- const moderation = await this.background.moderation.getValue();
147
- if (this.onboardingStatus.get() === OnboardingStatus.Completed) {
148
- return;
149
- }
150
- const onboardingEnabled = !!(moderation?.options?.onboardingEnabled && this.featureSettings.get().inplayGame?.onboarding?.completed);
151
- const optIn = !!this.featureSettings.get().inplayGame?.titleCard?.optIn;
152
- if (onboardingEnabled) {
153
- if (optIn) {
154
- this.onboardingStatus.set(OnboardingStatus.Required);
155
- }
156
- else {
157
- this.onboardingStatus.set(OnboardingStatus.Optional);
158
- }
159
- }
160
- else {
161
- if (optIn) {
162
- this.onboardingStatus.set(OnboardingStatus.Unavailable);
163
- }
164
- else {
165
- this.onboardingStatus.set(OnboardingStatus.Disabled);
166
- }
167
- }
168
- };
169
- showOnboardingInApp = () => {
170
- const { inplayGame } = this.featureSettings.get();
171
- if (!inplayGame) {
172
- return;
173
- }
174
- const { titleCard, overview } = inplayGame;
175
- this.notifications.add({
176
- type: NotificationType.ONBOARDING,
177
- id: this.background.getCurrentSessionId({ prefix: 'onboarding' }),
178
- action: this.openFeature,
179
- close: this.closeFeature,
180
- autoHideDuration: 100000,
181
- data: {
182
- header: titleCard?.header,
183
- title: titleCard?.title,
184
- subtitle: titleCard?.subtitle,
185
- graphicBg: titleCard?.appearance?.graphic,
186
- icon: titleCard?.media?.icon,
187
- sponsorLogo: titleCard?.media?.sponsorLogo,
188
- primaryColor: overview?.appearance?.primaryColor,
189
- },
190
- });
191
- };
192
122
  connect = (transport) => {
193
123
  this.userSummary.invalidate();
194
124
  this.leaderboardList.invalidate();
@@ -207,15 +137,18 @@ export class Gamification extends AbstractFeature {
207
137
  const question = await queries.getQuestionByUser(id, transport);
208
138
  const correctAnswer = question?.answers.find(({ correct }) => correct);
209
139
  this.notifications.add({
210
- type: NotificationType.ONBOARDING,
140
+ type: NotificationType.QUESTION_RESOLVED,
211
141
  action: () => this.openQuestion(id),
212
142
  close: () => this.closeQuestion(id),
213
143
  autoHideDuration: 5000,
214
144
  id: notificationId,
215
145
  data: {
216
- title: correctAnswer?.youVoted
217
- ? `Congratulations! You answered correctly! You won ${correctAnswer.points} pts!`
218
- : `Better luck next time! Correct: ${correctAnswer?.text}!`,
146
+ questionType: QuestionType.PREDICTION,
147
+ question: {
148
+ title: correctAnswer?.youVoted
149
+ ? `Congratulations! You answered correctly! You won ${correctAnswer.points} pts!`
150
+ : `Better luck next time! Correct: ${correctAnswer?.text}!`,
151
+ },
219
152
  },
220
153
  });
221
154
  }
@@ -230,19 +163,6 @@ export class Gamification extends AbstractFeature {
230
163
  disconnect = () => {
231
164
  this.background.feedSubscription.removeListener('feed-subscription-questions-list');
232
165
  };
233
- // onboarding
234
- submitInplay = async () => {
235
- const eventId = this.background.slStreamId.get();
236
- if (eventId) {
237
- await actions.submitInplay(this.transport, eventId);
238
- this.onboardingStatus.set(OnboardingStatus.Completed);
239
- this.storage.saveOnboardingStatus({
240
- organizationId: this.background.organizationId.get() || '',
241
- userId: this.background.userId.get() || '',
242
- eventId,
243
- }, OnboardingStatus.Completed);
244
- }
245
- };
246
166
  submitAnswer = async (questionId, answerId) => {
247
167
  await actions.submitAnswer(this.transport, { questionId, answerId });
248
168
  // Todo: add invalidate openedQuestion
package/lib/highlights.js CHANGED
@@ -37,14 +37,13 @@ export class Highlights extends AbstractFeature {
37
37
  type: NotificationType.QUESTION,
38
38
  action: () => question.data?.question && this.openHighlight(question.data.question.id),
39
39
  close: () => question.data?.question && this.closeHighlight(question.data.question.id),
40
- autoHideDuration: +(question.data.moderation?.question?.appearance?.autoHideInterval || '1000'),
40
+ autoHideDuration: 1000 * +(question.data.question?.appearance?.autoHideInterval || '5'),
41
41
  id: this.background.getCurrentSessionId({ prefix: 'notification', entity: question.data.question.id }),
42
42
  data: {
43
- title: question.data.question.notification.title,
44
- color: question.data.question.notification.indicatorColor,
45
- icon: question.data.question.notification.image,
46
- imageMode: question.data.question.notification.imageMode,
47
- imagePosition: question.data.question.notification.imagePosition,
43
+ questionType: question.data.question.type,
44
+ question: {
45
+ title: question.data.question.notification.title,
46
+ },
48
47
  },
49
48
  });
50
49
  }
@@ -0,0 +1,23 @@
1
+ import { Transport } from '@streamlayer/sdk-web-api';
2
+ import { type Notifications } from '@streamlayer/sdk-web-notifications';
3
+ import { GamificationBackground } from './background';
4
+ import { Gamification } from './';
5
+ /**
6
+ * Required: in-app should be displayed and questions not available
7
+ * Optional: in-app should be displayed but questions are available
8
+ * Completed: user completed onboarding, cached in browser. Linked by eventId, organizationId and userId
9
+ * Disabled: no in-app but questions are available
10
+ * Unavailable: no in-app and questions not available [behavior is discussed]
11
+ */
12
+ export declare enum OnboardingStatus {
13
+ Unset = "unset",
14
+ Required = "required",
15
+ Optional = "optional",
16
+ Completed = "completed",
17
+ Disabled = "disabled",
18
+ Unavailable = "unavailable"
19
+ }
20
+ export declare const onboarding: (service: Gamification, background: GamificationBackground, transport: Transport, notifications: Notifications) => {
21
+ $store: import("nanostores").WritableAtom<OnboardingStatus>;
22
+ submitInplay: () => Promise<void>;
23
+ };
@@ -0,0 +1,116 @@
1
+ import { createSingleStore } from '@streamlayer/sdk-web-interfaces';
2
+ import { NotificationType } from '@streamlayer/sdk-web-notifications';
3
+ import { QuestionType } from '@streamlayer/sdk-web-types';
4
+ import { GamificationStorage } from './storage';
5
+ import { submitInplay as submitInplayApi } from './queries/actions';
6
+ /**
7
+ * Required: in-app should be displayed and questions not available
8
+ * Optional: in-app should be displayed but questions are available
9
+ * Completed: user completed onboarding, cached in browser. Linked by eventId, organizationId and userId
10
+ * Disabled: no in-app but questions are available
11
+ * Unavailable: no in-app and questions not available [behavior is discussed]
12
+ */
13
+ export var OnboardingStatus;
14
+ (function (OnboardingStatus) {
15
+ OnboardingStatus["Unset"] = "unset";
16
+ OnboardingStatus["Required"] = "required";
17
+ OnboardingStatus["Optional"] = "optional";
18
+ OnboardingStatus["Completed"] = "completed";
19
+ OnboardingStatus["Disabled"] = "disabled";
20
+ OnboardingStatus["Unavailable"] = "unavailable";
21
+ })(OnboardingStatus || (OnboardingStatus = {}));
22
+ export const onboarding = (service, background, transport, notifications) => {
23
+ const storage = new GamificationStorage();
24
+ const $store = createSingleStore(OnboardingStatus.Unset);
25
+ const showOnboardingInApp = () => {
26
+ const { inplayGame = {} } = service.featureSettings.get();
27
+ const notificationId = background.getCurrentSessionId({ prefix: 'onboarding' });
28
+ notifications.add({
29
+ type: NotificationType.ONBOARDING,
30
+ id: notificationId,
31
+ action: service.openFeature,
32
+ close: () => {
33
+ notifications.markAsViewed(notificationId);
34
+ },
35
+ autoHideDuration: 1000000,
36
+ data: {
37
+ questionType: QuestionType.UNSET,
38
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
39
+ // @ts-ignore
40
+ onboarding: { ...inplayGame },
41
+ },
42
+ });
43
+ };
44
+ $store.subscribe((onboardingStatus) => {
45
+ if (onboardingStatus === OnboardingStatus.Optional || OnboardingStatus.Required) {
46
+ showOnboardingInApp();
47
+ }
48
+ if (onboardingStatus === OnboardingStatus.Completed) {
49
+ background.activeQuestionId.invalidate();
50
+ }
51
+ });
52
+ /**
53
+ * check onboarding status, sync with browser cache
54
+ * retrieve onboarding settings from api
55
+ */
56
+ const onboardingProcess = async () => {
57
+ const userId = background.userId.get();
58
+ if (!userId) {
59
+ return;
60
+ }
61
+ const onboardingStatus = storage.getOnboardingStatus({
62
+ userId,
63
+ organizationId: background.organizationId.get() || '',
64
+ eventId: background.slStreamId.get() || '',
65
+ });
66
+ if (onboardingStatus === OnboardingStatus.Completed) {
67
+ $store.set(OnboardingStatus.Completed);
68
+ }
69
+ const moderation = await background.moderation.getValue();
70
+ if ($store.get() === OnboardingStatus.Completed) {
71
+ return;
72
+ }
73
+ const onboardingEnabled = !!(moderation?.options?.onboardingEnabled && service.featureSettings.get().inplayGame?.onboarding?.completed);
74
+ const optIn = !!service.featureSettings.get().inplayGame?.titleCard?.optIn;
75
+ if (onboardingEnabled) {
76
+ if (optIn) {
77
+ $store.set(OnboardingStatus.Required);
78
+ }
79
+ else {
80
+ $store.set(OnboardingStatus.Optional);
81
+ }
82
+ }
83
+ else {
84
+ if (optIn) {
85
+ $store.set(OnboardingStatus.Unavailable);
86
+ }
87
+ else {
88
+ $store.set(OnboardingStatus.Disabled);
89
+ }
90
+ }
91
+ };
92
+ void onboardingProcess();
93
+ background.userId.subscribe((userId) => {
94
+ if (userId) {
95
+ void onboardingProcess();
96
+ }
97
+ });
98
+ background.moderation.subscribe((value) => {
99
+ if (value.data) {
100
+ void onboardingProcess();
101
+ }
102
+ });
103
+ const submitInplay = async () => {
104
+ const eventId = background.slStreamId.get();
105
+ if (eventId) {
106
+ await submitInplayApi(transport, eventId);
107
+ $store.set(OnboardingStatus.Completed);
108
+ storage.saveOnboardingStatus({
109
+ organizationId: background.organizationId.get() || '',
110
+ userId: background.userId.get() || '',
111
+ eventId,
112
+ }, OnboardingStatus.Completed);
113
+ }
114
+ };
115
+ return { $store, submitInplay };
116
+ };
package/lib/storage.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Storage } from '@streamlayer/sdk-web-storage';
2
- import { OnboardingStatus } from './gamification';
2
+ import { OnboardingStatus } from './onboarding';
3
3
  type UserProps = {
4
4
  userId: string;
5
5
  eventId: string;
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@streamlayer/feature-gamification",
3
- "version": "0.21.2",
3
+ "version": "0.23.0",
4
4
  "peerDependencies": {
5
5
  "@bufbuild/protobuf": "^1.4.2",
6
6
  "@streamlayer/sl-eslib": "^5.53.6",
7
7
  "@fastify/deepmerge": "*",
8
8
  "nanostores": "^0.9.5",
9
9
  "@streamlayer/sdk-web-api": "^0.0.1",
10
- "@streamlayer/sdk-web-core": "^0.17.5",
11
- "@streamlayer/sdk-web-interfaces": "^0.18.12",
12
- "@streamlayer/sdk-web-logger": "^0.0.2",
13
- "@streamlayer/sdk-web-notifications": "^0.10.11",
14
- "@streamlayer/sdk-web-storage": "^0.0.2",
15
- "@streamlayer/sdk-web-types": "^0.18.2"
10
+ "@streamlayer/sdk-web-interfaces": "^0.18.14",
11
+ "@streamlayer/sdk-web-logger": "^0.0.3",
12
+ "@streamlayer/sdk-web-core": "^0.17.7",
13
+ "@streamlayer/sdk-web-notifications": "^0.12.0",
14
+ "@streamlayer/sdk-web-storage": "^0.0.3",
15
+ "@streamlayer/sdk-web-types": "^0.20.0"
16
16
  },
17
17
  "devDependencies": {
18
18
  "tslib": "^2.6.2"