Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge simplified hook signatures #1024

Merged
merged 15 commits into from
Mar 3, 2023
Merged
23 changes: 16 additions & 7 deletions packages/commerce-sdk-react/src/auth/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import Auth from './'
import Auth, {AuthData} from './'
import jwt from 'jsonwebtoken'
import {helpers} from 'commerce-sdk-isomorphic'
import * as utils from '../utils'
Expand Down Expand Up @@ -39,6 +39,9 @@ jest.mock('../utils', () => ({
onClient: () => true
}))

/** The auth data we store has a slightly different shape than what we use. */
type StoredAuthData = Omit<AuthData, 'refresh_token'> & {refresh_token_guest?: string}

const config = {
clientId: 'clientId',
organizationId: 'organizationId',
Expand Down Expand Up @@ -85,7 +88,7 @@ describe('Auth', () => {
test('this.data returns the storage value', () => {
const auth = new Auth(config)

const sample = {
const sample: StoredAuthData = {
refresh_token_guest: 'refresh_token_guest',
access_token: 'access_token',
customer_id: 'customer_id',
Expand All @@ -97,7 +100,9 @@ describe('Auth', () => {
usid: 'usid',
customer_type: 'guest'
}
const {refresh_token_guest, ...result} = {...sample, refresh_token: 'refresh_token_guest'}
// Convert stored format to exposed format
const result = {...sample, refresh_token: 'refresh_token_guest'}
delete result.refresh_token_guest

Object.keys(sample).forEach((key) => {
// @ts-expect-error private method
Expand Down Expand Up @@ -161,7 +166,7 @@ describe('Auth', () => {
test('ready - re-use valid access token', () => {
const auth = new Auth(config)

const data = {
const data: StoredAuthData = {
refresh_token_guest: 'refresh_token_guest',
access_token: jwt.sign({exp: Math.floor(Date.now() / 1000) + 1000}, 'secret'),
customer_id: 'customer_id',
Expand All @@ -173,7 +178,9 @@ describe('Auth', () => {
usid: 'usid',
customer_type: 'guest'
}
const {refresh_token_guest, ...result} = {...data, refresh_token: 'refresh_token_guest'}
// Convert stored format to exposed format
const result = {...data, refresh_token: 'refresh_token_guest'}
delete result.refresh_token_guest

Object.keys(data).forEach((key) => {
// @ts-expect-error private method
Expand All @@ -192,7 +199,7 @@ describe('Auth', () => {
test('ready - use refresh token when access token is expired', async () => {
const auth = new Auth(config)

const data = {
const data: StoredAuthData = {
refresh_token_guest: 'refresh_token_guest',
access_token: jwt.sign({exp: Math.floor(Date.now() / 1000) - 1000}, 'secret'),
customer_id: 'customer_id',
Expand All @@ -204,7 +211,9 @@ describe('Auth', () => {
usid: 'usid',
customer_type: 'guest'
}
const {refresh_token_guest, ...result} = {...data, refresh_token: 'refresh_token_guest'}
// Convert stored format to exposed format
const result = {...data, refresh_token: 'refresh_token_guest'}
delete result.refresh_token_guest

Object.keys(data).forEach((key) => {
// @ts-expect-error private method
Expand Down
77 changes: 40 additions & 37 deletions packages/commerce-sdk-react/src/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, Salesforce, Inc.
* Copyright (c) 2023, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
Expand All @@ -12,11 +12,12 @@ import {
ShopperCustomersTypes
} from 'commerce-sdk-isomorphic'
import jwtDecode from 'jwt-decode'
import {ApiClientConfigParams, Argument} from '../hooks/types'
import {ApiClientConfigParams, Prettify, RemoveStringIndex} from '../hooks/types'
import {BaseStorage, LocalStorage, CookieStorage, MemoryStorage, StorageType} from './storage'
import {CustomerType} from '../hooks/useCustomerType'
import {onClient} from '../utils'

type TokenResponse = ShopperLoginTypes.TokenResponse
type Helpers = typeof helpers
interface AuthConfig extends ApiClientConfigParams {
redirectURI: string
Expand All @@ -30,24 +31,25 @@ interface JWTHeaders {
iat: number
}

// this type is slightly different from ShopperLoginTypes.TokenResponse, reasons:
// 1. TokenResponse is too generic (with & {[key:string]: any}), we need a more
// restrictive type to make sure type safe
// 2. The refresh tokens are stored separately for guest and registered user. Instead
// of refresh_token, we have refresh_token_guest and refresh_token_registered
/**
* The extended field is not from api response, we manually store the auth type,
* so we don't need to make another API call when we already have the data.
* Plus, the getCustomer endpoint only works for registered user, it returns a 404 for a guest user,
* and it's not easy to grab this info in user land, so we add it into the Auth object, and expose it via a hook
*/
export type AuthData = Prettify<
RemoveStringIndex<TokenResponse> & {
customer_type: CustomerType
idp_access_token: string
}
>

/** A shopper could be guest or registered, so we store the refresh tokens individually. */
type AuthDataKeys =
| 'access_token'
| 'customer_id'
| 'enc_user_id'
| 'expires_in'
| 'id_token'
| 'idp_access_token'
| Exclude<keyof AuthData, 'refresh_token'>
| 'refresh_token_guest'
| 'refresh_token_registered'
| 'token_type'
| 'usid'
| 'site_id'
| 'customer_type'

type AuthDataMap = Record<
AuthDataKeys,
{
Expand All @@ -57,16 +59,6 @@ type AuthDataMap = Record<
}
>

/**
* The extended field is not from api response, we manually store the auth type,
* so we don't need to make another API call when we already have the data.
* Plus, the getCustomer endpoint only works for registered user, it returns a 404 for a guest user,
* and it's not easy to grab this info in user land, so we add it into the Auth object, and expose it via a hook
*/
type AuthData = ShopperLoginTypes.TokenResponse & {
customer_type: CustomerType
}

/**
* A map of the data that this auth module stores. This maps the name of the property to
* the storage type and the key when stored in that storage. You can also pass in a "callback"
Expand Down Expand Up @@ -119,10 +111,6 @@ const DATA_MAP: AuthDataMap = {
store.delete('cc-nx-g')
}
},
site_id: {
storageType: 'cookie',
key: 'cc-site-id'
},
customer_type: {
storageType: 'local',
key: 'customer_type'
Expand All @@ -141,7 +129,7 @@ class Auth {
private client: ShopperLogin<ApiClientConfigParams>
private shopperCustomersClient: ShopperCustomers<ApiClientConfigParams>
private redirectURI: string
private pendingToken: Promise<ShopperLoginTypes.TokenResponse> | undefined
private pendingToken: Promise<TokenResponse> | undefined
private REFRESH_TOKEN_EXPIRATION_DAYS = 90
private stores: Record<StorageType, BaseStorage>
private fetchedToken: string
Expand Down Expand Up @@ -208,9 +196,10 @@ class Auth {
}

private clearStorage() {
Object.keys(DATA_MAP).forEach((keyName) => {
type Key = keyof AuthDataMap
const {key, storageType} = DATA_MAP[keyName as Key]
// Type assertion because Object.keys is silly and limited :(
const keys = Object.keys(DATA_MAP) as AuthDataKeys[]
keys.forEach((keyName) => {
const {key, storageType} = DATA_MAP[keyName]
const store = this.stores[storageType]
store.delete(key)
})
Expand Down Expand Up @@ -248,7 +237,7 @@ class Auth {
* This method stores the TokenResponse object retrived from SLAS, and
* store the data in storage.
*/
private handleTokenResponse(res: ShopperLoginTypes.TokenResponse, isGuest: boolean) {
private handleTokenResponse(res: TokenResponse, isGuest: boolean) {
this.set('access_token', res.access_token)
this.set('customer_id', res.customer_id)
this.set('enc_user_id', res.enc_user_id)
Expand All @@ -272,7 +261,7 @@ class Auth {
*
* @Internal
*/
async queueRequest(fn: () => Promise<ShopperLoginTypes.TokenResponse>, isGuest: boolean) {
async queueRequest(fn: () => Promise<TokenResponse>, isGuest: boolean) {
const queue = this.pendingToken ?? Promise.resolve()
this.pendingToken = queue.then(async () => {
const token = await fn()
Expand Down Expand Up @@ -331,6 +320,20 @@ class Auth {
)
}

/**
* Creates a function that only executes after a session is initialized.
* @param fn Function that needs to wait until the session is initialized.
* @returns Wrapped function
*/
whenReady<Args extends unknown[], Data>(
fn: (...args: Args) => Promise<Data>
): (...args: Args) => Promise<Data> {
return async (...args) => {
await this.ready()
return await fn(...args)
}
}

/**
* A wrapper method for commerce-sdk-isomorphic helper: loginGuestUser.
*
Expand Down
Loading