-
-
Notifications
You must be signed in to change notification settings - Fork 256
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(unlock-app): Fix Checkout
paywallConfig
Extraction and Validati…
…on from URL Query Parameters (#14699) * convert checkout content to client component * improve paywallConfig utility * update tests
- Loading branch information
Showing
3 changed files
with
92 additions
and
57 deletions.
There are no files selected for viewing
73 changes: 32 additions & 41 deletions
73
unlock-app/src/__tests__/utils/getConfigFromSearch.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,76 +1,67 @@ | ||
// TODO: remove, some part related to old checkout (callToAction) | ||
|
||
import { getPaywallConfigFromQuery } from '../../utils/paywallConfig' | ||
import { vi, beforeAll, afterAll, it, describe, expect } from 'vitest' | ||
|
||
let originalConsole: any | ||
let error = vi.fn() | ||
import { it, describe, expect } from 'vitest' | ||
|
||
const lock = '0x1234567890123456789012345678901234567890' | ||
const validConfig = { | ||
callToAction: { | ||
default: 'hi', | ||
expired: 'there', | ||
pending: 'pending', | ||
confirmed: 'confirmed', | ||
}, | ||
title: 'Valid Title', | ||
network: 1, | ||
pessimistic: true, | ||
skipRecipient: false, | ||
locks: { | ||
[lock]: { | ||
name: 'A Lock', | ||
}, | ||
}, | ||
icon: 'http://image.com/image.tiff', | ||
minRecipients: 1, | ||
maxRecipients: 5, | ||
} | ||
|
||
describe('getConfigFromSearch', () => { | ||
beforeAll(() => { | ||
originalConsole = global.console | ||
}) | ||
beforeEach(() => { | ||
error = vi.fn() | ||
;(global.console as any) = { error } | ||
}) | ||
afterAll(() => { | ||
global.console = originalConsole | ||
}) | ||
|
||
describe('getPaywallConfigFromQuery', () => { | ||
it('should be undefined if there is no paywall config', () => { | ||
expect.assertions(2) | ||
|
||
expect.assertions(1) | ||
expect(getPaywallConfigFromQuery({})).toBeUndefined() | ||
expect(error).not.toHaveBeenCalled() | ||
}) | ||
|
||
it('should be undefined if paywall config is malformed JSON', () => { | ||
expect.assertions(2) | ||
|
||
expect.assertions(1) | ||
expect(getPaywallConfigFromQuery({ paywallConfig: '{' })).toBeUndefined() | ||
expect(error).toHaveBeenCalledWith( | ||
'paywall config in URL not valid JSON, continuing with undefined' | ||
) | ||
}) | ||
|
||
it('should be undefined if paywall config does not pass validation', () => { | ||
expect.assertions(2) | ||
|
||
expect.assertions(1) | ||
expect(getPaywallConfigFromQuery({ paywallConfig: '{}' })).toBeUndefined() | ||
expect(error).toHaveBeenCalledWith( | ||
'paywall config in URL does not pass validation, continuing with undefined' | ||
) | ||
}) | ||
|
||
it('should return a paywall config otherwise', () => { | ||
expect.assertions(2) | ||
|
||
expect.assertions(1) | ||
expect( | ||
getPaywallConfigFromQuery({ | ||
paywallConfig: encodeURIComponent(JSON.stringify(validConfig)), | ||
paywallConfig: JSON.stringify(validConfig), | ||
}) | ||
).toEqual( | ||
expect.objectContaining({ | ||
icon: 'http://image.com/image.tiff', | ||
title: 'Valid Title', | ||
network: 1, | ||
pessimistic: true, | ||
skipRecipient: false, | ||
minRecipients: 1, | ||
maxRecipients: 5, | ||
}) | ||
) | ||
expect(error).not.toHaveBeenCalled() | ||
}) | ||
|
||
it('should handle ReadonlyURLSearchParams input', () => { | ||
expect.assertions(1) | ||
const searchParams = new URLSearchParams({ lock: '0x123', network: '1' }) | ||
const result = getPaywallConfigFromQuery(searchParams) | ||
expect(result).toEqual({ | ||
title: 'Unlock Protocol', | ||
network: 1, | ||
locks: { | ||
'0x123': {}, | ||
}, | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,41 +1,83 @@ | ||
import { PaywallConfigType } from '@unlock-protocol/core' | ||
import { isValidPaywallConfig } from './checkoutValidators' | ||
import { ReadonlyURLSearchParams } from 'next/navigation' | ||
|
||
/** | ||
* Extracts and validates the PaywallConfig from the query parameters. | ||
* | ||
* This utility handles two types of input: | ||
* 1. A full PaywallConfig object passed as a JSON string in the 'paywallConfig' parameter. | ||
* 2. A simplified configuration using 'lock', 'title', and 'network' parameters. | ||
* | ||
* It performs the following steps: | ||
* 1. Normalizes the input to a consistent object format. | ||
* 2. Attempts to parse and validate a full PaywallConfig if present. | ||
* 3. Constructs a simple PaywallConfig from individual parameters if no full config is found. | ||
* 4. Validates the resulting configuration. | ||
* | ||
* @param query - The URL query parameters as a Record<string, any> or ReadonlyURLSearchParams | ||
* @returns The validated PaywallConfigType or undefined if invalid | ||
*/ | ||
export function getPaywallConfigFromQuery( | ||
query: Record<string, any> | ||
query: Record<string, any> | ReadonlyURLSearchParams | ||
): PaywallConfigType | undefined { | ||
if (typeof query.paywallConfig === 'string') { | ||
const rawConfig = query.paywallConfig | ||
const decodedConfig = decodeURIComponent(rawConfig) | ||
let queryObj: Record<string, any> = {} | ||
|
||
// Convert ReadonlyURLSearchParams to Record<string, any> if necessary | ||
if (query instanceof URLSearchParams) { | ||
query.forEach((value, key) => { | ||
// If the key already exists, convert the value to an array | ||
if (queryObj[key]) { | ||
if (Array.isArray(queryObj[key])) { | ||
queryObj[key].push(value) | ||
} else { | ||
queryObj[key] = [queryObj[key], value] | ||
} | ||
} else { | ||
queryObj[key] = value | ||
} | ||
}) | ||
} else { | ||
queryObj = query | ||
} | ||
|
||
// Attempt to parse and validate a full PaywallConfig | ||
if (typeof queryObj.paywallConfig === 'string') { | ||
const rawConfig = queryObj.paywallConfig | ||
const decodedConfig = rawConfig | ||
|
||
let parsedConfig: any | ||
|
||
try { | ||
parsedConfig = JSON.parse(decodedConfig) | ||
parsedConfig.minRecipients = parsedConfig?.minRecipients || 1 | ||
parsedConfig.maxRecipients = parsedConfig?.maxRecipients || 1 | ||
// Use nullish coalescing operator to preserve null values | ||
parsedConfig.minRecipients = parsedConfig?.minRecipients ?? 1 | ||
parsedConfig.maxRecipients = parsedConfig?.maxRecipients ?? 1 | ||
} catch (e) { | ||
console.error( | ||
'paywall config in URL not valid JSON, continuing with undefined' | ||
) | ||
return undefined | ||
} | ||
|
||
if (isValidPaywallConfig(parsedConfig)) { | ||
return parsedConfig as PaywallConfigType | ||
} | ||
console.error( | ||
'paywall config in URL does not pass validation, continuing with undefined' | ||
) | ||
return undefined | ||
} | ||
if (typeof query.lock === 'string') { | ||
|
||
// Construct a simple PaywallConfig from individual parameters | ||
if (typeof queryObj.lock === 'string') { | ||
const lock = queryObj.lock | ||
const title = queryObj.title || 'Unlock Protocol' | ||
const network = Number(queryObj.network) | ||
|
||
return { | ||
title: query.title || 'Unlock Protocol', | ||
network: Number(query.network), | ||
title, | ||
network, | ||
locks: { | ||
[query.lock]: {}, | ||
[lock]: {}, | ||
}, | ||
} | ||
} | ||
|
||
// No valid configuration found | ||
return undefined | ||
} |