Skip to content
This repository was archived by the owner on Jul 3, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 61 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
1e136d3
feat: ECOM-80 add pagination to catalog page
merucoding Jun 11, 2025
422e085
fix: ECOM-80 reset to first page when searching
merucoding Jun 12, 2025
7424f00
build(deps): Bump brace-expansion
dependabot[bot] Jun 12, 2025
be06e90
Merge pull request #42 from bitbybit/dependabot/npm_and_yarn/npm_and_…
bitbybit Jun 14, 2025
e4a473d
feat: ECOM-78 add disabled state to add to cart button
merucoding Jun 14, 2025
09ea2fa
refactor: ECOM-77 resolve PR comments
merucoding Jun 14, 2025
7d90aa8
Merge pull request #41 from bitbybit/feature/ECOM-77
bitbybit Jun 14, 2025
d18f067
feat: ECOM-91 add about us page
Jun 14, 2025
dcbfd58
feat: ECOM-79 cart api
bitbybit Jun 15, 2025
ef3e0e5
Merge pull request #43 from bitbybit/feature/ECOM-79
merucoding Jun 15, 2025
84698b8
feat: ECOM-88 add message when cart is empty
merucoding Jun 15, 2025
985543f
Merge pull request #44 from bitbybit/feature/ECOM-77
bitbybit Jun 15, 2025
3ebee9a
refactor: ECOM-84 rename ITEMS_PER_PAGE
bitbybit Jun 15, 2025
2c2a08f
feat: ECOM-90 add clear shopping cart button
merucoding Jun 15, 2025
989cbb8
fix: ECOM-91 fix swiper styles
Jun 15, 2025
75ddcec
Merge branch 'release/basket-about_us' of https://github.com/bitbybit…
Jun 15, 2025
1471b08
Merge pull request #45 from bitbybit/feature/ECOM-77
bitbybit Jun 15, 2025
a6be736
feat: ECOM-85 add product item quantity control
merucoding Jun 15, 2025
f1f82c1
feat: ECOM-91 fix swiper
Jun 15, 2025
36ffc1e
feat: ECOM-84 cart store
bitbybit Jun 15, 2025
4d6cfa6
Merge pull request #46 from bitbybit/feature/ECOM-77
bitbybit Jun 15, 2025
221ac89
Merge pull request #47 from bitbybit/feature/ECOM-84
bitbybit Jun 15, 2025
2084400
Merge branch 'release/basket-about_us' of https://github.com/bitbybit…
Jun 15, 2025
c91cbb6
feat: ECOM-91 add remove/add button to product page
Jun 15, 2025
08d57df
feat: ECOM-89 discount codes
bitbybit Jun 15, 2025
3146248
fix: ECOM-91 fix images, separate files
Jun 15, 2025
6de07be
Merge pull request #49 from bitbybit/feature/ECOM-89
bitbybit Jun 16, 2025
23873bc
feat: ECOM-83 add cart item view, delete cart item button
merucoding Jun 16, 2025
5fe1144
fix: ECOM-91 fix name of file
Jun 16, 2025
ad0c549
Merge pull request #48 from bitbybit/feature/ECOM-91
bitbybit Jun 16, 2025
2f27a14
fix: ECOM-91 fix test
Jun 16, 2025
19f129f
fix: ECOM-91 fix test
Jun 16, 2025
67f197f
feat: ECOM-77 merge commit
merucoding Jun 16, 2025
22a1310
Merge pull request #52 from bitbybit/feature/ECOM-91
bitbybit Jun 16, 2025
6902b68
feat: ECOM-83 add cart top panel
merucoding Jun 16, 2025
fc3b47d
Merge branch 'release/basket-about_us' of https://github.com/bitbybit…
merucoding Jun 16, 2025
ea9f9ab
fix: ECOM-77 fix tests
merucoding Jun 16, 2025
4b725bc
feat: ECOM-91 add Quantity Indicator to header
Jun 16, 2025
77e1993
refactor: ECOM-83 resolve PR comments
merucoding Jun 16, 2025
4ebd337
feat: ECOM-83 clear cart, update cart quantity
bitbybit Jun 16, 2025
83f5691
chore: ECOM-83 error message text typo
bitbybit Jun 16, 2025
3d9c012
Merge pull request #54 from bitbybit/feature/ECOM-83
bitbybit Jun 16, 2025
6a08b2b
Merge branch 'release/basket-about_us' of https://github.com/bitbybit…
merucoding Jun 16, 2025
4a35f66
refactor: ECOM-91 use totalLineItemQuantity instead of reduce
Jun 16, 2025
7e81399
feat: ECOM-83 clear cart, update item quantity
merucoding Jun 16, 2025
d6b5d25
refactor: ECOM-91 change color theme
Jun 16, 2025
6320378
Merge branch 'release/basket-about_us' into feature/ECOM-91
zhuravel17 Jun 16, 2025
799c469
Merge pull request #51 from bitbybit/feature/ECOM-77
bitbybit Jun 16, 2025
804eb17
refactor: ECOM-91 change color theme in notFound files
Jun 16, 2025
edeb263
Merge pull request #53 from bitbybit/feature/ECOM-91
zhuravel17 Jun 16, 2025
a36d898
Merge branch 'release/basket-about_us' of https://github.com/bitbybit…
Jun 16, 2025
e0f2e65
fix: ECOM-91 fix discounts layout
Jun 16, 2025
fd8f115
Merge pull request #55 from bitbybit/feature/ECOM-91
zhuravel17 Jun 16, 2025
c77242c
refactor: ECOM-89 add some styles to discount card
merucoding Jun 16, 2025
af514b1
Merge pull request #56 from bitbybit/feature/ECOM-77
bitbybit Jun 16, 2025
bc6480d
fix: ECOM-83 cart item info flex box, Discounts header
merucoding Jun 17, 2025
f908707
Merge pull request #57 from bitbybit/fix/ECOM-83
bitbybit Jun 17, 2025
d89ff34
refactor: ECOM-83 code style PR updates
bitbybit Jun 17, 2025
5e3d53a
fix: fix test
Jun 17, 2025
88ada10
Merge pull request #59 from bitbybit/fix-test
bitbybit Jun 17, 2025
b9e928c
Merge pull request #58 from bitbybit/pr-fixes
bitbybit Jun 17, 2025
c85601f
test: ECOM-83 add cart item view, empty cart, quantity control tests
merucoding Jun 17, 2025
6120099
Merge pull request #60 from bitbybit/test/ECOM-83
bitbybit Jun 17, 2025
d185efe
test: ECOM-83 fix empty cart test
merucoding Jun 18, 2025
1d5c509
Merge pull request #61 from bitbybit/test/ECOM-83
bitbybit Jun 18, 2025
2221631
fix: ECOM-83 crosscheck fixes
bitbybit Jun 19, 2025
2c8bb5a
Merge pull request #62 from bitbybit/crosscheck-fixes
bitbybit Jun 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions app/api/TokenCache.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type TokenStore, type TokenCache } from '@commercetools/ts-client'

export class SessionStorageTokenCache implements TokenCache {
export class LocalStorageTokenCache implements TokenCache {
private readonly key: string

constructor(key: string) {
Expand All @@ -19,25 +19,25 @@ export class SessionStorageTokenCache implements TokenCache {
}

public get(): TokenStore {
const raw = globalThis.sessionStorage.getItem(this.key)
const raw = globalThis.localStorage.getItem(this.key)
const value: unknown = JSON.parse(String(raw))

if (value === null) {
return { token: '', expirationTime: 0 }
}

if (!SessionStorageTokenCache.isTokenStore(value)) {
if (!LocalStorageTokenCache.isTokenStore(value)) {
throw new Error('Token cache not found')
}

return value
}

public set(cache: TokenStore): void {
globalThis.sessionStorage.setItem(this.key, JSON.stringify(cache))
globalThis.localStorage.setItem(this.key, JSON.stringify(cache))
}

public remove(): void {
globalThis.sessionStorage.removeItem(this.key)
globalThis.localStorage.removeItem(this.key)
}
}
129 changes: 98 additions & 31 deletions app/api/client.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { type AuthMiddlewareOptions, ClientBuilder, type HttpMiddlewareOptions } from '@commercetools/ts-client'
import { ClientBuilder, type HttpMiddlewareOptions } from '@commercetools/ts-client'
import {
type ByProjectKeyMeRequestBuilder,
type ByProjectKeyRequestBuilder,
type ClientResponse,
createApiBuilderFromCtpClient,
type Customer,
type CustomerSignInResult
} from '@commercetools/platform-sdk'
import { SessionStorageTokenCache } from '~/api/TokenCache'
import { LocalStorageTokenCache } from '~/api/TokenCache'

type ApiClientProperties = {
type ApiClientProps = {
authUri?: string
baseUri?: string
clientId?: string
Expand Down Expand Up @@ -42,6 +43,8 @@ type SignupPayload = {
password: string
}

export const LANG = 'en-US'

export class CtpApiClient {
private readonly authUri: string
private readonly baseUri: string
Expand All @@ -50,9 +53,12 @@ export class CtpApiClient {
private readonly projectKey: string
private readonly scopes: string

private readonly tokenCache = new SessionStorageTokenCache('token')
private readonly publicTokenCache = new LocalStorageTokenCache('public')
private readonly protectedTokenCache = new LocalStorageTokenCache('protected')

private readonly anonymousIdStorageKey = 'anonymous_id'

private readonly public: ByProjectKeyRequestBuilder
private public: ByProjectKeyRequestBuilder
private protected?: ByProjectKeyRequestBuilder
private current: ByProjectKeyRequestBuilder

Expand All @@ -63,7 +69,7 @@ export class CtpApiClient {
clientSecret = String(import.meta.env.VITE_CTP_CLIENT_SECRET),
projectKey = String(import.meta.env.VITE_CTP_PROJECT_KEY),
scopes = String(import.meta.env.VITE_CTP_SCOPES)
}: ApiClientProperties = {}) {
}: ApiClientProps = {}) {
this.authUri = authUri
this.baseUri = baseUri
this.clientId = clientId
Expand All @@ -74,7 +80,7 @@ export class CtpApiClient {
this.public = this.createPublic()

if (this.hasToken) {
this.protected = this.createPublic(true)
this.protected = this.createProtectedWithToken()
this.current = this.protected
} else {
this.current = this.public
Expand All @@ -87,38 +93,53 @@ export class CtpApiClient {

public get hasToken(): boolean {
try {
return this.tokenCache.get().token !== ''
return this.protectedTokenCache.get().token !== ''
} catch {
return false
}
}

public async login(email: string, password: string): Promise<ClientResponse<Customer>> {
this.logout()
await this.public
.me()
.login()
.post({
body: {
email,
password,
activeCartSignInMode: 'UseAsNewActiveCustomerCart',
updateProductData: true
}
})
.execute()

this.protected = this.createProtected(email, password)
this.protected = this.createProtectedWithCredentials(email, password)
this.current = this.protected

return await this.getCurrentCustomer()
}

public logout(): void {
this.tokenCache.remove()
this.current = this.public
this.protectedTokenCache.remove()
this.publicTokenCache.remove()
this.public = this.createPublic()
this.protected = undefined
this.current = this.public
}

public getCurrentCustomerBuilder(): ByProjectKeyMeRequestBuilder {
return this.current.me()
}

public async getCurrentCustomer(): Promise<ClientResponse<Customer>> {
return await this.current.me().get().execute()
return await this.getCurrentCustomerBuilder().get().execute()
}

public async signup(payload: SignupPayload): Promise<ClientResponse<CustomerSignInResult>> {
this.logout()

const billingAddressIndex = payload.addresses.findIndex(({ type }) => type === CUSTOMER_ADDRESS_TYPE.BILLING)
const shippingAddressIndex = payload.addresses.findIndex(({ type }) => type === CUSTOMER_ADDRESS_TYPE.SHIPPING)

return this.current
return this.public
.me()
.signup()
.post({
Expand Down Expand Up @@ -154,22 +175,23 @@ export class CtpApiClient {
}
}

private createPublic(withTokenCache: boolean = false): ByProjectKeyRequestBuilder {
const authOptions: AuthMiddlewareOptions = {
credentials: { clientId: this.clientId, clientSecret: this.clientSecret },
host: this.authUri,
httpClient: fetch,
projectKey: this.projectKey,
scopes: [this.scopes]
}

if (withTokenCache) {
authOptions.tokenCache = this.tokenCache
}
private createPublic(): ByProjectKeyRequestBuilder {
const anonymousId = this.getOrCreateAnonymousId()

const client = new ClientBuilder()
.withProjectKey(this.projectKey)
.withClientCredentialsFlow(authOptions)
.withAnonymousSessionFlow({
credentials: {
clientId: this.clientId,
clientSecret: this.clientSecret,
anonymousId
},
host: this.authUri,
httpClient: fetch,
projectKey: this.projectKey,
scopes: [this.scopes],
tokenCache: this.publicTokenCache
})
.withHttpMiddleware(this.getHttpOptions())
.withLoggerMiddleware()
.build()
Expand All @@ -179,7 +201,7 @@ export class CtpApiClient {
})
}

private createProtected(email: string, password: string): ByProjectKeyRequestBuilder {
private createProtectedWithCredentials(email: string, password: string): ByProjectKeyRequestBuilder {
const client = new ClientBuilder()
.withProjectKey(this.projectKey)
.withPasswordFlow({
Expand All @@ -188,7 +210,7 @@ export class CtpApiClient {
httpClient: fetch,
projectKey: this.projectKey,
scopes: [this.scopes],
tokenCache: this.tokenCache
tokenCache: this.protectedTokenCache
})
.withHttpMiddleware(this.getHttpOptions())
.withLoggerMiddleware()
Expand All @@ -198,6 +220,51 @@ export class CtpApiClient {
projectKey: this.projectKey
})
}

private createProtectedWithToken(): ByProjectKeyRequestBuilder {
const { refreshToken } = this.protectedTokenCache.get()

if (refreshToken === undefined) {
throw new Error('Refresh token is missing')
}

const client = new ClientBuilder()
.withProjectKey(this.projectKey)
.withRefreshTokenFlow({
credentials: { clientId: this.clientId, clientSecret: this.clientSecret },
host: this.authUri,
httpClient: fetch,
projectKey: this.projectKey,
tokenCache: this.protectedTokenCache,
refreshToken
})
.withHttpMiddleware(this.getHttpOptions())
.withLoggerMiddleware()
.build()

return createApiBuilderFromCtpClient(client).withProjectKey({
projectKey: this.projectKey
})
}

private getOrCreateAnonymousId(): string {
let id = this.getAnonymousIdFromStorage()

if (id === null || !this.hasToken) {
id = crypto.randomUUID()
this.saveAnonymousIdToStorage(id)
}

return id
}

private getAnonymousIdFromStorage(): string | null {
return localStorage.getItem(this.anonymousIdStorageKey)
}

private saveAnonymousIdToStorage(id: string): void {
localStorage.setItem(this.anonymousIdStorageKey, id)
}
}

export const ctpApiClient = new CtpApiClient()
Loading