@streamlayer/feature-gamification 0.22.0 → 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,37 +49,26 @@ 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
- });
93
67
  /**
94
68
  * listen for active question and show in-app notification
95
69
  */
96
70
  this.background.activeQuestionId.listen((question) => {
97
- if (question && question.data && this.onboardingStatus.get()) {
71
+ if (question && question.data && this.onboardingStatus.$store.get()) {
98
72
  if (question.data.question?.id !== undefined &&
99
73
  question.data.question.notification !== undefined &&
100
74
  question.data.moderation?.bypassNotifications?.inAppSilence !== SilenceSetting.ON &&
@@ -144,84 +118,7 @@ export class Gamification extends AbstractFeature {
144
118
  }
145
119
  }
146
120
  });
147
- void this.onboardingProcess();
148
- this.background.userId.subscribe((userId) => {
149
- if (userId) {
150
- void this.onboardingProcess();
151
- }
152
- });
153
- this.background.moderation.subscribe((value) => {
154
- if (value.data) {
155
- void this.onboardingProcess();
156
- }
157
- });
158
121
  }
159
- /**
160
- * check onboarding status, sync with browser cache
161
- * retrieve onboarding settings from api
162
- */
163
- onboardingProcess = async () => {
164
- const userId = this.background.userId.get();
165
- if (!userId) {
166
- return;
167
- }
168
- const onboardingStatus = this.storage.getOnboardingStatus({
169
- userId,
170
- organizationId: this.background.organizationId.get() || '',
171
- eventId: this.background.slStreamId.get() || '',
172
- });
173
- if (onboardingStatus === OnboardingStatus.Completed) {
174
- this.onboardingStatus.set(OnboardingStatus.Completed);
175
- }
176
- const moderation = await this.background.moderation.getValue();
177
- if (this.onboardingStatus.get() === OnboardingStatus.Completed) {
178
- return;
179
- }
180
- const onboardingEnabled = !!(moderation?.options?.onboardingEnabled && this.featureSettings.get().inplayGame?.onboarding?.completed);
181
- const optIn = !!this.featureSettings.get().inplayGame?.titleCard?.optIn;
182
- if (onboardingEnabled) {
183
- if (optIn) {
184
- this.onboardingStatus.set(OnboardingStatus.Required);
185
- }
186
- else {
187
- this.onboardingStatus.set(OnboardingStatus.Optional);
188
- }
189
- }
190
- else {
191
- if (optIn) {
192
- this.onboardingStatus.set(OnboardingStatus.Unavailable);
193
- }
194
- else {
195
- this.onboardingStatus.set(OnboardingStatus.Disabled);
196
- }
197
- }
198
- };
199
- showOnboardingInApp = () => {
200
- const { inplayGame } = this.featureSettings.get();
201
- if (!inplayGame) {
202
- return;
203
- }
204
- const { titleCard, overview } = inplayGame;
205
- this.notifications.add({
206
- type: NotificationType.ONBOARDING,
207
- id: this.background.getCurrentSessionId({ prefix: 'onboarding' }),
208
- action: this.openFeature,
209
- close: this.closeFeature,
210
- autoHideDuration: 100000,
211
- data: {
212
- questionType: QuestionType.UNSET,
213
- onboarding: {
214
- header: titleCard?.header,
215
- title: titleCard?.title,
216
- subtitle: titleCard?.subtitle,
217
- graphicBg: titleCard?.appearance?.graphic,
218
- icon: titleCard?.media?.icon,
219
- sponsorLogo: titleCard?.media?.sponsorLogo,
220
- primaryColor: overview?.appearance?.primaryColor,
221
- },
222
- },
223
- });
224
- };
225
122
  connect = (transport) => {
226
123
  this.userSummary.invalidate();
227
124
  this.leaderboardList.invalidate();
@@ -266,19 +163,6 @@ export class Gamification extends AbstractFeature {
266
163
  disconnect = () => {
267
164
  this.background.feedSubscription.removeListener('feed-subscription-questions-list');
268
165
  };
269
- // onboarding
270
- submitInplay = async () => {
271
- const eventId = this.background.slStreamId.get();
272
- if (eventId) {
273
- await actions.submitInplay(this.transport, eventId);
274
- this.onboardingStatus.set(OnboardingStatus.Completed);
275
- this.storage.saveOnboardingStatus({
276
- organizationId: this.background.organizationId.get() || '',
277
- userId: this.background.userId.get() || '',
278
- eventId,
279
- }, OnboardingStatus.Completed);
280
- }
281
- };
282
166
  submitAnswer = async (questionId, answerId) => {
283
167
  await actions.submitAnswer(this.transport, { questionId, answerId });
284
168
  // Todo: add invalidate openedQuestion
@@ -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.22.0",
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.6",
11
- "@streamlayer/sdk-web-interfaces": "^0.18.13",
12
- "@streamlayer/sdk-web-logger": "^0.0.2",
13
- "@streamlayer/sdk-web-notifications": "^0.11.0",
14
- "@streamlayer/sdk-web-storage": "^0.0.2",
15
- "@streamlayer/sdk-web-types": "^0.19.0"
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"