1
- import { Injectable } from '@nestjs/common'
1
+ import { Injectable , Logger } from '@nestjs/common'
2
2
import { IdentityProvider } from '@prisma/client'
3
3
import { ApiCoreService , ellipsify } from '@pubkey-network/api-core-data-access'
4
+ import { ApiProtocolService } from '@pubkey-network/api-protocol-data-access'
5
+ import { OnboardingStep } from './entity/onboarding-step'
6
+ import { OnboardingRequirements } from './entity/onboarding.entity'
4
7
5
8
@Injectable ( )
6
9
export class ApiOnboardingService {
7
- constructor ( private readonly core : ApiCoreService ) { }
10
+ readonly socialProviders = [
11
+ IdentityProvider . Discord ,
12
+ IdentityProvider . Github ,
13
+ IdentityProvider . Google ,
14
+ // IdentityProvider.Telegram,
15
+ IdentityProvider . X ,
16
+ ]
17
+
18
+ private readonly logger = new Logger ( ApiOnboardingService . name )
19
+ constructor ( private readonly core : ApiCoreService , private readonly protocol : ApiProtocolService ) { }
8
20
9
21
async getOnboardingUsernames ( userId : string ) {
10
- const found = await this . core . data . user . findUnique ( {
11
- where : { id : userId } ,
12
- include : { identities : true } ,
13
- } )
14
- if ( ! found ) {
15
- throw new Error ( `User ${ userId } not found` )
16
- }
17
- const usernames = found . identities
22
+ const user = await this . core . ensureUserById ( userId )
23
+ const rawUsernames = user . identities
18
24
. filter ( ( i ) => i ?. profile )
19
25
. map ( ( i ) => i . profile as { username ?: string } )
20
26
// Remove any identities that don't have a username
21
27
. filter ( ( i ) => i . username )
22
28
// Take the username property
23
29
. map ( ( i ) => i . username as string )
24
- // Take the first part of any email addresses
25
- . map ( ( i ) => i ?. split ( '@' ) [ 0 ] )
26
- // Convert any special characters to lowercase
27
- . map ( ( i ) => i . replace ( / [ ^ a - z 0 - 9 ] / gi, '_' ) . toLowerCase ( ) )
30
+
31
+ const usernames = cleanupUsernames ( rawUsernames )
28
32
29
33
// For all usernames with an underscore, also offer the username without the underscore
30
34
for ( const username of usernames ) {
@@ -33,32 +37,29 @@ export class ApiOnboardingService {
33
37
}
34
38
}
35
39
36
- const wallets = found . identities
40
+ // Remove any duplicates and sort the usernames
41
+ const sortedUsernames = Array . from ( new Set ( usernames ) ) . sort ( )
42
+
43
+ // Create usernames based on Solana wallets for users that like to be pseudonymous
44
+ const wallets = user . identities
37
45
// Remove any identities that are not Solana wallets
38
46
. filter ( ( i ) => i ?. provider === IdentityProvider . Solana )
39
47
// Take the providerId property
40
48
. map ( ( i ) => i . providerId as string )
41
49
// Convert any special characters to lowercase
42
50
. map ( ( i ) => ellipsify ( i , 4 , '__' ) . toLowerCase ( ) )
43
51
44
- usernames . push ( ...wallets )
52
+ // Remove any duplicates and sort the wallets
53
+ const sortedWallets = Array . from ( new Set ( wallets ) ) . sort ( )
45
54
46
- // Remove any duplicates and sort the usernames
47
- return Array . from ( new Set ( usernames ) ) . sort ( )
55
+ return [ ...sortedUsernames , ...sortedWallets ]
48
56
}
49
57
50
58
async getOnboardingAvatarUrls ( userId : string ) {
51
- const found = await this . core . data . user . findUnique ( {
52
- where : { id : userId } ,
53
- include : { identities : true } ,
54
- } )
55
- if ( ! found ) {
56
- throw new Error ( `User ${ userId } not found` )
57
- }
58
-
59
+ const user = await this . core . ensureUserById ( userId )
59
60
const usernames : string [ ] = [ ]
60
61
61
- const avatarUrls = found . identities
62
+ const avatarUrls = user . identities
62
63
. filter ( ( i ) => i ?. profile )
63
64
. map ( ( i ) => i . profile as { avatarUrl ?: string ; username ?: string } )
64
65
. map ( ( i ) => {
@@ -76,14 +77,98 @@ export class ApiOnboardingService {
76
77
const cleaned = cleanupUsernames ( usernames )
77
78
78
79
for ( const username of cleaned ) {
80
+ avatarUrls . push (
81
+ `https://api.dicebear.com/9.x/avataaars/svg?backgroundColor=b6e3f4,c0aede,d1d4f9&seed=${ username } ` ,
82
+ )
79
83
avatarUrls . push ( `https://api.dicebear.com/9.x/initials/svg?backgroundColor=b6e3f4,c0aede,d1d4f9&seed=${ username } ` )
80
84
}
81
85
82
86
// Remove any duplicates and sort the avatarUrls
83
87
return Array . from ( new Set ( avatarUrls ) )
84
88
}
89
+
90
+ async getOnboardingRequirements ( userId : string ) : Promise < OnboardingRequirements > {
91
+ const user = await this . core . ensureUserById ( userId )
92
+
93
+ const [ usernames , avatarUrls ] = await Promise . all ( [
94
+ this . getOnboardingUsernames ( userId ) ,
95
+ this . getOnboardingAvatarUrls ( userId ) ,
96
+ ] )
97
+
98
+ const socialIdentities = user . identities . filter ( ( i ) => identityProvidersSocial . includes ( i ?. provider ) )
99
+ const solanaIdentities = user . identities . filter ( ( i ) => i ?. provider === IdentityProvider . Solana )
100
+ const validAvatarUrl = avatarUrls . includes ( user . avatarUrl ?? '' )
101
+ const validUsername = usernames . includes ( user . username )
102
+ const profileAccount = user . profile ?? null
103
+
104
+ let step : OnboardingStep
105
+
106
+ if ( ! socialIdentities . length ) {
107
+ step = OnboardingStep . LinkSocialIdentities
108
+ } else if ( ! solanaIdentities . length ) {
109
+ step = OnboardingStep . LinkSolanaWallets
110
+ } else if ( ! validAvatarUrl || ! validUsername ) {
111
+ step = OnboardingStep . CustomizeProfile
112
+ } else if ( ! user . profile ) {
113
+ step = OnboardingStep . CreateProfile
114
+ } else {
115
+ step = OnboardingStep . Finished
116
+ }
117
+
118
+ return {
119
+ profileAccount,
120
+ socialIdentities : socialIdentities . length ?? 0 ,
121
+ solanaIdentities : solanaIdentities . length ?? 0 ,
122
+ validAvatarUrl : avatarUrls . includes ( user . avatarUrl ?? '' ) ,
123
+ validUsername : usernames . includes ( user . username ) ,
124
+ step,
125
+ }
126
+ }
127
+
128
+ async createProfile ( userId : string , publicKey : string ) {
129
+ const user = await this . core . ensureUserById ( userId )
130
+ const requirementsMet = await this . getOnboardingRequirements ( user . id )
131
+ if ( ! requirementsMet ) {
132
+ throw new Error ( `User ${ user . id } has not met the onboarding requirements` )
133
+ }
134
+
135
+ return this . protocol . createUserProfile ( user . id , publicKey )
136
+ }
137
+
138
+ async customizeProfile ( userId : string , username : string , avatarUrl : string ) {
139
+ const user = await this . core . ensureUserById ( userId )
140
+ const [ usernames , avatarUrls ] = await Promise . all ( [
141
+ this . getOnboardingUsernames ( user . id ) ,
142
+ this . getOnboardingAvatarUrls ( user . id ) ,
143
+ ] )
144
+
145
+ if ( ! usernames . includes ( username ) ) {
146
+ throw new Error ( `User ${ user . username } has not met the onboarding requirements` )
147
+ }
148
+
149
+ if ( ! avatarUrls . includes ( avatarUrl ) ) {
150
+ throw new Error ( `User ${ user . username } has not met the onboarding requirements` )
151
+ }
152
+
153
+ try {
154
+ const updated = await this . core . updateUserById ( user . id , { username, avatarUrl } )
155
+ this . logger . log ( `User ${ user . username } has been updated to ${ updated . username } and ${ updated . avatarUrl } ` )
156
+ return true
157
+ } catch ( error ) {
158
+ console . error ( error )
159
+ throw new Error ( `User ${ user . username } could not be updated` )
160
+ }
161
+ }
85
162
}
86
163
164
+ const identityProvidersSocial : IdentityProvider [ ] = [
165
+ IdentityProvider . Discord ,
166
+ IdentityProvider . Github ,
167
+ IdentityProvider . Google ,
168
+ IdentityProvider . Telegram ,
169
+ IdentityProvider . X ,
170
+ ]
171
+
87
172
function cleanupUsernames ( usernames : string [ ] ) {
88
173
return (
89
174
usernames // Take the first part of any email addresses
0 commit comments