• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

input-output-hk / lace / 8521699122

02 Apr 2024 11:02AM UTC coverage: 53.437% (-0.4%) from 53.839%
8521699122

push

github

f055f2
pczeglik-iohk
chore: cardano services point out live envs

2293 of 5275 branches covered (43.47%)

Branch coverage included in aggregate %.

5239 of 8820 relevant lines covered (59.4%)

54.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.43
/apps/browser-extension-wallet/src/lib/scripts/background/services/userIdService.ts
1
import { WalletManagerApi, WalletRepositoryApi, WalletType } from '@cardano-sdk/web-extension';
2✔
2
import { Wallet } from '@lace/cardano';
3
import { BehaviorSubject, ReplaySubject, combineLatest, distinctUntilChanged } from 'rxjs';
2✔
4
import { getActiveWallet, hashExtendedAccountPublicKey } from '@lib/scripts/background/util';
2✔
5
import { UserId, UserIdService as UserIdServiceInterface } from '@lib/scripts/types';
6
import randomBytes from 'randombytes';
2✔
7
import { UserTrackingType } from '@providers/AnalyticsProvider/analyticsTracker';
2✔
8
import isUndefined from 'lodash/isUndefined';
2✔
9
import { clearBackgroundStorage, getBackgroundStorage, setBackgroundStorage } from '../storage';
2✔
10

11
export type UserIdServiceStorage = {
12
  get: typeof getBackgroundStorage;
13
  set: typeof setBackgroundStorage;
14
  clear: typeof clearBackgroundStorage;
15
};
16

17
// eslint-disable-next-line no-magic-numbers
18
export const SESSION_LENGTH = Number(process.env.SESSION_LENGTH_IN_SECONDS || 1800) * 1000;
2✔
19
export const USER_ID_BYTE_SIZE = 8;
2✔
20

21
export class UserIdService implements UserIdServiceInterface {
2✔
22
  private randomizedUserId?: string;
23
  private walletBasedUserId?: string;
24
  private sessionTimeout?: NodeJS.Timeout;
25
  private userIdRestored = false;
24✔
26
  public userId$: ReplaySubject<UserId> = new ReplaySubject();
24✔
27
  private userTrackingType$: BehaviorSubject<UserTrackingType> = new BehaviorSubject(UserTrackingType.Basic);
24✔
28
  private hasNewSessionStarted = false;
24✔
29

30
  constructor(
31
    private walletRepository: WalletRepositoryApi<Wallet.WalletMetadata, Wallet.AccountMetadata>,
24✔
32
    private walletManager: WalletManagerApi,
24✔
33
    private storage: UserIdServiceStorage = {
24!
34
      clear: clearBackgroundStorage,
35
      get: getBackgroundStorage,
36
      set: setBackgroundStorage
37
    },
38
    private sessionLength: number = SESSION_LENGTH
24✔
39
  ) {
40
    combineLatest([
24✔
41
      this.userTrackingType$.pipe(distinctUntilChanged()),
42
      this.walletManager.activeWalletId$.pipe(
43
        distinctUntilChanged((a, b) => a?.walletId === b?.walletId && a?.accountIndex === b?.accountIndex)
×
44
      )
45
    ]).subscribe(async ([type]) => {
44✔
46
      const id = await this.getUserId();
44✔
47
      this.userId$.next({
44✔
48
        type,
49
        id
50
      });
51
    });
52
  }
53

54
  async init(): Promise<void> {
55
    if (!this.userIdRestored) {
76✔
56
      console.debug('[ANALYTICS] Restoring user ID...');
30✔
57
      await this.restoreUserId();
30✔
58
    }
59

60
    if (!this.randomizedUserId) {
76✔
61
      console.debug('[ANALYTICS] User ID not found - generating new one');
22✔
62
      this.randomizedUserId = randomBytes(USER_ID_BYTE_SIZE).toString('hex');
22✔
63
    }
64
  }
65

66
  private async getWalletBasedUserId(): Promise<string | undefined> {
67
    const active = await getActiveWallet({
60✔
68
      walletManager: this.walletManager,
69
      walletRepository: this.walletRepository
70
    });
71
    if (!active) return;
60✔
72
    if (active.wallet.type === WalletType.Script || !active.account) {
30!
73
      throw new Error('Script wallet support not implemented');
×
74
    }
75
    const { usePersistentUserId } = await this.storage.get();
30✔
76

77
    if (!usePersistentUserId) {
30✔
78
      return undefined;
14✔
79
    }
80

81
    if (!this.walletBasedUserId) {
16✔
82
      this.walletBasedUserId = this.generateWalletBasedUserId(active.account.extendedAccountPublicKey);
6✔
83

84
      if (this.userTrackingType$.value !== UserTrackingType.Enhanced) {
6✔
85
        this.userTrackingType$.next(UserTrackingType.Enhanced);
2✔
86
      }
87
    }
88
    console.debug(`[ANALYTICS] getwalletBasedUserId() called (current Wallet Based ID: ${this.walletBasedUserId})`);
16✔
89
    // eslint-disable-next-line consistent-return
90
    return this.walletBasedUserId;
16✔
91
  }
92

93
  // TODO: make this method private when Motamo is not longer in use
94
  async getRandomizedUserId(): Promise<string> {
95
    await this.init();
72✔
96

97
    console.debug(`[ANALYTICS] getId() called (current ID: ${this.randomizedUserId})`);
72✔
98
    return this.randomizedUserId;
72✔
99
  }
100

101
  async getUserId(): Promise<string> {
102
    const walletBasedId = await this.getWalletBasedUserId();
54✔
103

104
    if (!walletBasedId) {
54✔
105
      return await this.getRandomizedUserId();
40✔
106
    }
107

108
    return walletBasedId;
14✔
109
  }
110

111
  async getAliasProperties(): Promise<{ alias: string; id: string }> {
112
    const id = await this.getWalletBasedUserId();
6✔
113
    const alias = await this.getRandomizedUserId();
6✔
114
    return { alias, id };
6✔
115
  }
116

117
  async resetToDefaultValues(): Promise<void> {
118
    const { usePersistentUserId, userId } = await this.storage.get();
×
119
    if (isUndefined(usePersistentUserId) && isUndefined(userId)) {
×
120
      await this.clearId();
×
121
      this.userIdRestored = false;
×
122
    }
123
  }
124

125
  async clearId(): Promise<void> {
126
    console.debug('[ANALYTICS] clearId() called');
2✔
127
    this.randomizedUserId = undefined;
2✔
128
    this.walletBasedUserId = undefined;
2✔
129
    this.clearSessionTimeout();
2✔
130
    this.hasNewSessionStarted = false;
2✔
131
    await this.storage.clear({ keys: ['userId', 'usePersistentUserId'] });
2✔
132
    this.userTrackingType$.next(UserTrackingType.Basic);
2✔
133
  }
134

135
  async makePersistent(): Promise<void> {
136
    console.debug('[ANALYTICS] Converting user ID into persistent');
4✔
137
    this.clearSessionTimeout();
4✔
138
    this.setSessionTimeout();
4✔
139
    const userId = await this.getRandomizedUserId();
4✔
140
    await this.storage.set({ usePersistentUserId: true, userId });
4✔
141
    this.userId$.next({
4✔
142
      id: userId,
143
      type: UserTrackingType.Enhanced
144
    });
145
    this.userTrackingType$.next(UserTrackingType.Enhanced);
4✔
146
  }
147

148
  async makeTemporary(): Promise<void> {
149
    console.debug('[ANALYTICS] Converting user ID into temporary');
4✔
150
    await this.storage.set({ usePersistentUserId: false, userId: undefined });
4✔
151
    this.setSessionTimeout();
4✔
152
    this.userTrackingType$.next(UserTrackingType.Basic);
4✔
153
  }
154

155
  async extendLifespan(): Promise<void> {
156
    console.debug('[ANALYTICS] Extending temporary ID lifespan');
2✔
157
    this.clearSessionTimeout();
2✔
158
    this.setSessionTimeout();
2✔
159
  }
160

161
  private async restoreUserId(): Promise<void> {
162
    const { userId, usePersistentUserId } = await this.storage.get();
30✔
163

164
    if (usePersistentUserId) {
30✔
165
      console.debug('[ANALYTICS] Restoring user ID from extension storage');
8✔
166
      this.randomizedUserId = userId;
8✔
167
    }
168

169
    this.userIdRestored = true;
30✔
170
    const trackingType = usePersistentUserId ? UserTrackingType.Enhanced : UserTrackingType.Basic;
30✔
171
    if (trackingType !== this.userTrackingType$.value) {
30✔
172
      this.userTrackingType$.next(trackingType);
8✔
173
    }
174
  }
175

176
  private setSessionTimeout(): void {
177
    if (this.sessionTimeout) {
10!
178
      return;
×
179
    }
180
    this.sessionTimeout = setTimeout(() => {
10✔
181
      if (this.userTrackingType$.value === UserTrackingType.Basic) {
4✔
182
        this.randomizedUserId = undefined;
4✔
183
      }
184
      this.hasNewSessionStarted = false;
4✔
185
    }, this.sessionLength);
186
  }
187

188
  private clearSessionTimeout(): void {
189
    clearTimeout(this.sessionTimeout);
8✔
190
    this.sessionTimeout = undefined;
8✔
191
  }
192

193
  generateWalletBasedUserId(extendedAccountPublicKey: Wallet.Crypto.Bip32PublicKeyHex): string {
194
    console.debug('[ANALYTICS] Wallet based ID not found - generating new one');
6✔
195
    // by requirement, we want to hash the extended account public key twice
196
    const hash = hashExtendedAccountPublicKey(extendedAccountPublicKey);
6✔
197
    return hashExtendedAccountPublicKey(hash);
6✔
198
  }
199

200
  async isNewSession(): Promise<boolean> {
201
    const isNewSession = !this.hasNewSessionStarted;
×
202
    this.hasNewSessionStarted = true;
×
203
    return isNewSession;
×
204
  }
205
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc