Skip to content
This repository was archived by the owner on Jul 3, 2025. It is now read-only.

Commit d972255

Browse files
authored
Merge pull request #50 from bitbybit/release/basket-about_us
Sprint 4: Basket Page, Catalog Page Enhancements, and About Us Page Implementation
2 parents 4db2224 + 2c8bb5a commit d972255

File tree

127 files changed

+2885
-950
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

127 files changed

+2885
-950
lines changed

app/api/TokenCache.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type TokenStore, type TokenCache } from '@commercetools/ts-client'
22

3-
export class SessionStorageTokenCache implements TokenCache {
3+
export class LocalStorageTokenCache implements TokenCache {
44
private readonly key: string
55

66
constructor(key: string) {
@@ -19,25 +19,25 @@ export class SessionStorageTokenCache implements TokenCache {
1919
}
2020

2121
public get(): TokenStore {
22-
const raw = globalThis.sessionStorage.getItem(this.key)
22+
const raw = globalThis.localStorage.getItem(this.key)
2323
const value: unknown = JSON.parse(String(raw))
2424

2525
if (value === null) {
2626
return { token: '', expirationTime: 0 }
2727
}
2828

29-
if (!SessionStorageTokenCache.isTokenStore(value)) {
29+
if (!LocalStorageTokenCache.isTokenStore(value)) {
3030
throw new Error('Token cache not found')
3131
}
3232

3333
return value
3434
}
3535

3636
public set(cache: TokenStore): void {
37-
globalThis.sessionStorage.setItem(this.key, JSON.stringify(cache))
37+
globalThis.localStorage.setItem(this.key, JSON.stringify(cache))
3838
}
3939

4040
public remove(): void {
41-
globalThis.sessionStorage.removeItem(this.key)
41+
globalThis.localStorage.removeItem(this.key)
4242
}
4343
}

app/api/client.ts

Lines changed: 164 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
import { type AuthMiddlewareOptions, ClientBuilder, type HttpMiddlewareOptions } from '@commercetools/ts-client'
1+
import { ClientBuilder, type HttpMiddlewareOptions } from '@commercetools/ts-client'
22
import {
3+
type ByProjectKeyMeRequestBuilder,
4+
type ApiRequest,
35
type ByProjectKeyRequestBuilder,
46
type ClientResponse,
57
createApiBuilderFromCtpClient,
68
type Customer,
79
type CustomerSignInResult
810
} from '@commercetools/platform-sdk'
9-
import { SessionStorageTokenCache } from '~/api/TokenCache'
11+
import { LocalStorageTokenCache } from '~/api/TokenCache'
1012

11-
type ApiClientProperties = {
13+
type ApiClientProps = {
1214
authUri?: string
1315
baseUri?: string
1416
clientId?: string
@@ -42,6 +44,8 @@ type SignupPayload = {
4244
password: string
4345
}
4446

47+
export const LANG = 'en-US'
48+
4549
export class CtpApiClient {
4650
private readonly authUri: string
4751
private readonly baseUri: string
@@ -50,9 +54,12 @@ export class CtpApiClient {
5054
private readonly projectKey: string
5155
private readonly scopes: string
5256

53-
private readonly tokenCache = new SessionStorageTokenCache('token')
57+
private readonly publicTokenCache = new LocalStorageTokenCache('public')
58+
private readonly protectedTokenCache = new LocalStorageTokenCache('protected')
59+
60+
private readonly anonymousIdStorageKey = 'anonymous_id'
5461

55-
private readonly public: ByProjectKeyRequestBuilder
62+
private public: ByProjectKeyRequestBuilder
5663
private protected?: ByProjectKeyRequestBuilder
5764
private current: ByProjectKeyRequestBuilder
5865

@@ -63,7 +70,7 @@ export class CtpApiClient {
6370
clientSecret = String(import.meta.env.VITE_CTP_CLIENT_SECRET),
6471
projectKey = String(import.meta.env.VITE_CTP_PROJECT_KEY),
6572
scopes = String(import.meta.env.VITE_CTP_SCOPES)
66-
}: ApiClientProperties = {}) {
73+
}: ApiClientProps = {}) {
6774
this.authUri = authUri
6875
this.baseUri = baseUri
6976
this.clientId = clientId
@@ -74,7 +81,7 @@ export class CtpApiClient {
7481
this.public = this.createPublic()
7582

7683
if (this.hasToken) {
77-
this.protected = this.createPublic(true)
84+
this.protected = this.createProtectedWithToken()
7885
this.current = this.protected
7986
} else {
8087
this.current = this.public
@@ -87,62 +94,101 @@ export class CtpApiClient {
8794

8895
public get hasToken(): boolean {
8996
try {
90-
return this.tokenCache.get().token !== ''
97+
return this.protectedTokenCache.get().token !== ''
9198
} catch {
9299
return false
93100
}
94101
}
95102

103+
private static isAnonymousIdError(error: unknown): boolean {
104+
return (
105+
typeof error === 'object' &&
106+
error !== null &&
107+
'statusCode' in error &&
108+
error.statusCode === 400 &&
109+
'message' in error &&
110+
typeof error.message === 'string' &&
111+
error.message.includes('anonymousId')
112+
)
113+
}
114+
96115
public async login(email: string, password: string): Promise<ClientResponse<Customer>> {
97-
this.logout()
116+
const request: () => ApiRequest<CustomerSignInResult> = () =>
117+
this.public
118+
.me()
119+
.login()
120+
.post({
121+
body: {
122+
email,
123+
password,
124+
activeCartSignInMode: 'UseAsNewActiveCustomerCart',
125+
updateProductData: true
126+
}
127+
})
128+
129+
try {
130+
await request().execute()
131+
} catch (error) {
132+
await this.handleError<CustomerSignInResult>({ error, request })
133+
}
98134

99-
this.protected = this.createProtected(email, password)
135+
this.protected = this.createProtectedWithCredentials(email, password)
100136
this.current = this.protected
101137

102138
return await this.getCurrentCustomer()
103139
}
104140

105141
public logout(): void {
106-
this.tokenCache.remove()
107-
this.current = this.public
142+
this.protectedTokenCache.remove()
143+
this.publicTokenCache.remove()
144+
this.public = this.createPublic()
108145
this.protected = undefined
146+
this.current = this.public
147+
}
148+
149+
public getCurrentCustomerBuilder(): ByProjectKeyMeRequestBuilder {
150+
return this.current.me()
109151
}
110152

111153
public async getCurrentCustomer(): Promise<ClientResponse<Customer>> {
112-
return await this.current.me().get().execute()
154+
return await this.getCurrentCustomerBuilder().get().execute()
113155
}
114156

115157
public async signup(payload: SignupPayload): Promise<ClientResponse<CustomerSignInResult>> {
116-
this.logout()
117-
118158
const billingAddressIndex = payload.addresses.findIndex(({ type }) => type === CUSTOMER_ADDRESS_TYPE.BILLING)
119159
const shippingAddressIndex = payload.addresses.findIndex(({ type }) => type === CUSTOMER_ADDRESS_TYPE.SHIPPING)
120160

121-
return this.current
122-
.me()
123-
.signup()
124-
.post({
125-
body: {
126-
addresses: payload.addresses.map(
127-
(address): Omit<CustomerAddress, 'type'> => ({
128-
city: address.city,
129-
country: address.country,
130-
firstName: payload.firstName,
131-
lastName: payload.lastName,
132-
postalCode: address.postalCode,
133-
streetName: address.streetName
134-
})
135-
),
136-
dateOfBirth: payload.dateOfBirth,
137-
defaultBillingAddress: billingAddressIndex === -1 ? undefined : billingAddressIndex,
138-
defaultShippingAddress: shippingAddressIndex === -1 ? undefined : shippingAddressIndex,
139-
email: payload.email,
140-
firstName: payload.firstName,
141-
lastName: payload.lastName,
142-
password: payload.password
143-
}
144-
})
145-
.execute()
161+
const request: () => ApiRequest<CustomerSignInResult> = () =>
162+
this.public
163+
.me()
164+
.signup()
165+
.post({
166+
body: {
167+
addresses: payload.addresses.map(
168+
(address): Omit<CustomerAddress, 'type'> => ({
169+
city: address.city,
170+
country: address.country,
171+
firstName: payload.firstName,
172+
lastName: payload.lastName,
173+
postalCode: address.postalCode,
174+
streetName: address.streetName
175+
})
176+
),
177+
dateOfBirth: payload.dateOfBirth,
178+
defaultBillingAddress: billingAddressIndex === -1 ? undefined : billingAddressIndex,
179+
defaultShippingAddress: shippingAddressIndex === -1 ? undefined : shippingAddressIndex,
180+
email: payload.email,
181+
firstName: payload.firstName,
182+
lastName: payload.lastName,
183+
password: payload.password
184+
}
185+
})
186+
187+
try {
188+
return await request().execute()
189+
} catch (error) {
190+
return await this.handleError<CustomerSignInResult>({ error, request })
191+
}
146192
}
147193

148194
private getHttpOptions(): HttpMiddlewareOptions {
@@ -154,22 +200,23 @@ export class CtpApiClient {
154200
}
155201
}
156202

157-
private createPublic(withTokenCache: boolean = false): ByProjectKeyRequestBuilder {
158-
const authOptions: AuthMiddlewareOptions = {
159-
credentials: { clientId: this.clientId, clientSecret: this.clientSecret },
160-
host: this.authUri,
161-
httpClient: fetch,
162-
projectKey: this.projectKey,
163-
scopes: [this.scopes]
164-
}
165-
166-
if (withTokenCache) {
167-
authOptions.tokenCache = this.tokenCache
168-
}
203+
private createPublic(): ByProjectKeyRequestBuilder {
204+
const anonymousId = this.getOrCreateAnonymousId()
169205

170206
const client = new ClientBuilder()
171207
.withProjectKey(this.projectKey)
172-
.withClientCredentialsFlow(authOptions)
208+
.withAnonymousSessionFlow({
209+
credentials: {
210+
clientId: this.clientId,
211+
clientSecret: this.clientSecret,
212+
anonymousId
213+
},
214+
host: this.authUri,
215+
httpClient: fetch,
216+
projectKey: this.projectKey,
217+
scopes: [this.scopes],
218+
tokenCache: this.publicTokenCache
219+
})
173220
.withHttpMiddleware(this.getHttpOptions())
174221
.withLoggerMiddleware()
175222
.build()
@@ -179,7 +226,7 @@ export class CtpApiClient {
179226
})
180227
}
181228

182-
private createProtected(email: string, password: string): ByProjectKeyRequestBuilder {
229+
private createProtectedWithCredentials(email: string, password: string): ByProjectKeyRequestBuilder {
183230
const client = new ClientBuilder()
184231
.withProjectKey(this.projectKey)
185232
.withPasswordFlow({
@@ -188,7 +235,33 @@ export class CtpApiClient {
188235
httpClient: fetch,
189236
projectKey: this.projectKey,
190237
scopes: [this.scopes],
191-
tokenCache: this.tokenCache
238+
tokenCache: this.protectedTokenCache
239+
})
240+
.withHttpMiddleware(this.getHttpOptions())
241+
.withLoggerMiddleware()
242+
.build()
243+
244+
return createApiBuilderFromCtpClient(client).withProjectKey({
245+
projectKey: this.projectKey
246+
})
247+
}
248+
249+
private createProtectedWithToken(): ByProjectKeyRequestBuilder {
250+
const { refreshToken } = this.protectedTokenCache.get()
251+
252+
if (refreshToken === undefined) {
253+
throw new Error('Refresh token is missing')
254+
}
255+
256+
const client = new ClientBuilder()
257+
.withProjectKey(this.projectKey)
258+
.withRefreshTokenFlow({
259+
credentials: { clientId: this.clientId, clientSecret: this.clientSecret },
260+
host: this.authUri,
261+
httpClient: fetch,
262+
projectKey: this.projectKey,
263+
tokenCache: this.protectedTokenCache,
264+
refreshToken
192265
})
193266
.withHttpMiddleware(this.getHttpOptions())
194267
.withLoggerMiddleware()
@@ -198,6 +271,42 @@ export class CtpApiClient {
198271
projectKey: this.projectKey
199272
})
200273
}
274+
275+
private async handleError<T>({
276+
error,
277+
request
278+
}: {
279+
error: unknown
280+
request: () => ApiRequest<T>
281+
}): Promise<ClientResponse<T>> {
282+
if (!CtpApiClient.isAnonymousIdError(error)) {
283+
throw error
284+
}
285+
286+
console.log('Anonymous ID is already in use. Creating new anonymous ID...')
287+
288+
this.logout()
289+
return await request().execute()
290+
}
291+
292+
private getOrCreateAnonymousId(): string {
293+
let id = this.getAnonymousIdFromStorage()
294+
295+
if (id === null || !this.hasToken) {
296+
id = crypto.randomUUID()
297+
this.saveAnonymousIdToStorage(id)
298+
}
299+
300+
return id
301+
}
302+
303+
private getAnonymousIdFromStorage(): string | null {
304+
return localStorage.getItem(this.anonymousIdStorageKey)
305+
}
306+
307+
private saveAnonymousIdToStorage(id: string): void {
308+
localStorage.setItem(this.anonymousIdStorageKey, id)
309+
}
201310
}
202311

203312
export const ctpApiClient = new CtpApiClient()

0 commit comments

Comments
 (0)