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

feat: allows UPS to receive cookie config options #261

Merged
merged 10 commits into from
Jun 7, 2024
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# [1.17.0-ups-configurable-cookie.1](https://github.com/mParticle/aquarium/compare/v1.16.1...v1.17.0-ups-configurable-cookie.1) (2024-06-03)


### Features

* allows UPS to receive cookie config options instead of only the key ([517d030](https://github.com/mParticle/aquarium/commit/517d03066bb1d55968d650bb7a2a6162a1567ad7))

## [1.16.1](https://github.com/mParticle/aquarium/compare/v1.16.0...v1.16.1) (2024-06-03)


Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mparticle/aquarium",
"version": "1.16.1",
"version": "1.17.0-ups-configurable-cookie.1",
"description": "mParticle Component Library",
"license": "Apache-2.0",
"keywords": [
Expand Down
15 changes: 5 additions & 10 deletions src/services/user-preferences/user-preferences-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,8 @@ describe('When testing the User Preferences Service', () => {
userPreferencesService = new UserPreferencesService<TestUserPreferenceId>(
definitions,
compositeUserPreferencesService,
cookieKey,
lowLevelScope,
() => new Date(),
{ key: cookieKey },
)
await userPreferencesService.init()

Expand Down Expand Up @@ -82,9 +81,8 @@ describe('When testing the User Preferences Service', () => {
userPreferencesService = new UserPreferencesService<TestUserPreferenceId>(
definitions,
compositeUserPreferencesService,
cookieKey,
currentScope,
() => new Date(),
{ key: cookieKey },
)
await userPreferencesService.init()

Expand All @@ -109,9 +107,8 @@ describe('When testing the User Preferences Service', () => {
userPreferencesService = new UserPreferencesService<TestUserPreferenceId>(
definitions,
compositeUserPreferencesService,
cookieKey,
someScope,
() => new Date(),
{ key: cookieKey },
)
await userPreferencesService.init()

Expand Down Expand Up @@ -148,9 +145,8 @@ describe('When testing the User Preferences Service', () => {
userPreferencesService = new UserPreferencesService<TestUserPreferenceId>(
definitions,
compositeUserPreferencesService,
cookieKey,
lowLevelScope,
() => new Date(),
{ key: cookieKey },
)
await userPreferencesService.init()

Expand Down Expand Up @@ -182,9 +178,8 @@ describe('When testing the User Preferences Service', () => {
userPreferencesService = new UserPreferencesService<TestUserPreferenceId>(
definitions,
compositeUserPreferencesService,
cookieKey,
lowLevelScope,
() => new Date(),
{ key: cookieKey },
)
await userPreferencesService.init()

Expand Down
15 changes: 6 additions & 9 deletions src/services/user-preferences/user-preferences.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
/* eslint-disable @typescript-eslint/no-extraneous-class,no-unused-vars,@typescript-eslint/no-unused-vars */
import * as Cookies from '../../utils/Cookies'
import { type UserPreferences } from 'src/services/user-preferences/models/storage-models/user-preferences'
import { type CompositeUserPreferences } from 'src/services/user-preferences/models/user-preferences/composite-user-preferences'
import { type UserPreferenceScope } from 'src/services/user-preferences/models/storage-models/user-preference-scope'
import { type UserPreferenceDefinitions } from 'src/services/user-preferences/models/definitions/user-preference-definitions'
import { type CompositeUserPreferencesService } from 'src/services/user-preferences/composite-user-preferences-service'
import * as Cookies from 'src/utils/Cookies'
import { type CookieOptions } from 'src/utils/Cookies'

export class UserPreferencesService<TUserPreferenceId extends PropertyKey> {
public preferences!: CompositeUserPreferences<TUserPreferenceId>

constructor(
private readonly definitions: UserPreferenceDefinitions<TUserPreferenceId>,
private readonly compositeUserPreferencesService: CompositeUserPreferencesService<TUserPreferenceId>,
private readonly cookieKey: string,
private readonly currentScope: UserPreferenceScope,
public dateFormatter: () => Date,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for dateFormatter anymore cause that was used for the expires option which is now configurable from the client.

private readonly cookieOptions: CookieOptions & { key: string },
private readonly onUpdate?: (resolvedPreferences: CompositeUserPreferences<TUserPreferenceId>) => void,
) {}

Expand Down Expand Up @@ -43,7 +43,7 @@ export class UserPreferencesService<TUserPreferenceId extends PropertyKey> {
// @ts-expect-error
const { allowedScope } = this.definitions[userPreferenceId]

const currentStoredPreferences = Cookies.getObject(this.cookieKey)
const currentStoredPreferences = Cookies.getObject(this.cookieOptions.key)

const storedPreferences = this.compositeUserPreferencesService.getUpdatedUserPreferenceStorageObject(
userPreferenceId,
Expand All @@ -69,14 +69,11 @@ export class UserPreferencesService<TUserPreferenceId extends PropertyKey> {
}

private async getStoredPreferences(): Promise<UserPreferences<TUserPreferenceId>> {
return await Promise.resolve(Cookies.getObject(this.cookieKey) ?? {})
return await Promise.resolve(Cookies.getObject(this.cookieOptions.key) ?? {})
}

private async setStoredPreferences(storedPreferences: UserPreferences<TUserPreferenceId>): Promise<void> {
Cookies.putObject(this.cookieKey, storedPreferences, {
expires: this.dateFormatter(),
path: '/',
})
Cookies.putObject(this.cookieOptions.key, storedPreferences, this.cookieOptions)

await Promise.resolve()
}
Expand Down
18 changes: 13 additions & 5 deletions src/utils/Cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,31 @@ export function getObject(key: string): string | null {
return value ? JSON.parse(value) : value
}

export function put(key: string, value: string | null, options: any /* TODO fix any */ = {}): void {
export type CookieOptions = {
path?: string
domain?: string
expires?: string | Date
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: string is for iso format? maybe restricting to Date only would make it clear for the consumers.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. Yeah string is for iso format cause the code where this came from originally supported both.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually made it only support string given that's what the cookie expires option needs to be set to.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought Would be good to have a flag, like permanent that sets the cookie for, say, 100 years. That way the client doesn't always have to calculate the "permanent" date.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it, although I don't think we should be doing Date manipulation inside the cookie library so I'm proposing we set it to a fixed value in the future.

secure?: boolean
}

export function put(key: string, value: string | null, options: CookieOptions = {}): void {
const defaultExpires = 'Thu, 01 Jan 1970 00:00:01 GMT'
let expires = options.expires
if (value == null) expires = 'Thu, 01 Jan 1970 00:00:01 GMT'
if (value == null) expires = defaultExpires
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question Not sure I understand the business requirement here. Comments would be good. A private function encapsulating the business rule would be good, too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I don't think I understand it either 😅 This is copied from the aurelia-plugins-cookies codebase.

By just looking at the code, I think there's 2 options: either the cookie is born invalid already or it never expires. Running some tests to see.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I figured it out by testing the implementation directly in the browser.

Turns out this defaultExpires is only used for the remove method since it doesn't actually "remove" the cookie but rather set it's value to null and call this put method. This makes it so that the expiration date goes to the default one and invalidates the cookie.

if (typeof expires === 'string') expires = new Date(expires)
let str = `${_encode(key)}=${value != null ? _encode(value) : ''}`
if (options.path) str += `; path=${options.path}`
if (options.domain) str += `; domain=${options.domain}`
if (options.expires) str += `; expires=${expires.toUTCString()}`
if (options.expires) str += `; expires=${expires?.toUTCString() ?? defaultExpires}`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise Much better.

if (options.secure) str += '; secure'
document.cookie = str
}

export function putObject(key: string, value: Record<string, unknown>, options = {}): void {
export function putObject(key: string, value: Record<string, unknown>, options: CookieOptions = {}): void {
put(key, JSON.stringify(value), options)
}

export function remove(key: string, options = {}): void {
export function remove(key: string, options: CookieOptions = {}): void {
put(key, null, options)
}

Expand Down
Loading