Skip to content

Commit

Permalink
fix(unlock-app): Fix Checkout paywallConfig Extraction and Validati…
Browse files Browse the repository at this point in the history
…on from URL Query Parameters (#14699)

* convert checkout content to client component

* improve paywallConfig utility

* update tests
  • Loading branch information
0xTxbi authored and julien51 committed Sep 30, 2024
1 parent e311379 commit 532f3c5
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 57 deletions.
73 changes: 32 additions & 41 deletions unlock-app/src/__tests__/utils/getConfigFromSearch.test.ts
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': {},
},
})
})
})
2 changes: 2 additions & 0 deletions unlock-app/src/components/interface/checkout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client'

import { useSearchParams } from 'next/navigation'
import { useEffect } from 'react'
import { useAuthenticate } from '~/hooks/useAuthenticate'
Expand Down
74 changes: 58 additions & 16 deletions unlock-app/src/utils/paywallConfig.ts
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
}

0 comments on commit 532f3c5

Please sign in to comment.