Barazo default frontend barazo.forum
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: wire up tosUrl for tos_acceptance onboarding fields (#160)

* feat(onboarding): render tosUrl as clickable link for tos_acceptance fields

Split tos_acceptance from custom_checkbox case in OnboardingFieldInput
so that when config.tosUrl is set, a "Read full Terms of Service" link
renders below the checkbox label. Opens in new tab with noopener noreferrer.

* feat(admin): add tosUrl input for tos_acceptance field type

Show a conditional "Terms of Service URL" input in the onboarding field
form when the field type is tos_acceptance. Clears config.tosUrl when
the input is emptied. Admins can now link to an external ToS document.

authored by

Guido X Jansen and committed by
GitHub
c44540a3 5e6031a7

+174 -1
+57
src/app/admin/onboarding/page.test.tsx
··· 229 229 expect(screen.getByText(/this field is required by the barazo platform/i)).toBeInTheDocument() 230 230 }) 231 231 232 + it('shows ToS URL input when field type is tos_acceptance', async () => { 233 + const user = userEvent.setup() 234 + render(<AdminOnboardingPage />) 235 + await user.click(screen.getByRole('button', { name: /add field/i })) 236 + // Default type is custom_text -- no ToS URL input yet 237 + expect(screen.queryByLabelText(/terms of service url/i)).not.toBeInTheDocument() 238 + // Switch to tos_acceptance 239 + await user.selectOptions(screen.getByLabelText(/field type/i), 'tos_acceptance') 240 + expect(screen.getByLabelText(/terms of service url/i)).toBeInTheDocument() 241 + }) 242 + 243 + it('hides ToS URL input for other field types', async () => { 244 + const user = userEvent.setup() 245 + render(<AdminOnboardingPage />) 246 + await user.click(screen.getByRole('button', { name: /add field/i })) 247 + await user.selectOptions(screen.getByLabelText(/field type/i), 'custom_text') 248 + expect(screen.queryByLabelText(/terms of service url/i)).not.toBeInTheDocument() 249 + }) 250 + 251 + it('saves tosUrl in config when provided', async () => { 252 + let capturedBody: Record<string, unknown> | null = null 253 + server.use( 254 + http.post('/api/admin/onboarding-fields', async ({ request }) => { 255 + capturedBody = (await request.json()) as Record<string, unknown> 256 + return HttpResponse.json( 257 + { 258 + id: 'new-field-1', 259 + communityDid: 'did:plc:test', 260 + fieldType: 'tos_acceptance', 261 + label: 'Accept ToS', 262 + description: '', 263 + isMandatory: true, 264 + sortOrder: 3, 265 + source: 'admin', 266 + config: capturedBody?.['config'] ?? null, 267 + createdAt: '2026-02-15T12:00:00.000Z', 268 + updatedAt: '2026-02-15T12:00:00.000Z', 269 + }, 270 + { status: 201 } 271 + ) 272 + }) 273 + ) 274 + const user = userEvent.setup() 275 + render(<AdminOnboardingPage />) 276 + await user.click(screen.getByRole('button', { name: /add field/i })) 277 + await user.selectOptions(screen.getByLabelText(/field type/i), 'tos_acceptance') 278 + await user.type(screen.getByLabelText(/^label$/i), 'Accept ToS') 279 + await user.type(screen.getByLabelText(/terms of service url/i), 'https://example.com/tos') 280 + await user.click(screen.getByRole('button', { name: /^save$/i })) 281 + await waitFor(() => { 282 + expect(capturedBody).not.toBeNull() 283 + }) 284 + expect(capturedBody).toMatchObject({ 285 + config: { tosUrl: 'https://example.com/tos' }, 286 + }) 287 + }) 288 + 232 289 it('passes axe accessibility check', async () => { 233 290 const { container } = render(<AdminOnboardingPage />) 234 291 await waitFor(() => {
+28
src/components/admin/onboarding/onboarding-field-form.tsx
··· 102 102 placeholder="Additional context or instructions for users" 103 103 /> 104 104 </div> 105 + {editing.fieldType === 'tos_acceptance' && ( 106 + <div> 107 + <label htmlFor="field-tos-url" className="block text-sm font-medium text-foreground"> 108 + Terms of Service URL (optional) 109 + </label> 110 + <input 111 + id="field-tos-url" 112 + type="url" 113 + value={typeof editing.config?.tosUrl === 'string' ? editing.config.tosUrl : ''} 114 + onChange={(e) => 115 + onChange({ 116 + ...editing, 117 + config: e.target.value 118 + ? { ...editing.config, tosUrl: e.target.value } 119 + : (() => { 120 + const { tosUrl: _, ...rest } = editing.config ?? {} 121 + return Object.keys(rest).length > 0 ? rest : null 122 + })(), 123 + }) 124 + } 125 + className="mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground" 126 + placeholder="https://example.com/terms" 127 + /> 128 + <p className="mt-0.5 text-xs text-muted-foreground"> 129 + Link displayed alongside the checkbox so users can review your terms. 130 + </p> 131 + </div> 132 + )} 105 133 <div className="flex items-center gap-2"> 106 134 <input 107 135 id="field-mandatory"
+34 -1
src/components/onboarding-field-input.tsx
··· 72 72 </div> 73 73 ) 74 74 75 - case 'tos_acceptance': 75 + case 'tos_acceptance': { 76 + const tosUrl = typeof field.config?.tosUrl === 'string' ? field.config.tosUrl : null 77 + return ( 78 + <div className="flex items-start gap-2"> 79 + <input 80 + id={labelId} 81 + type="checkbox" 82 + checked={value === true} 83 + onChange={(e) => onChange(e.target.checked)} 84 + className="mt-1 h-4 w-4 rounded border-border" 85 + /> 86 + <div> 87 + <FieldLabel 88 + htmlFor={labelId} 89 + label={field.label} 90 + required={required} 91 + description={field.description} 92 + block={false} 93 + /> 94 + {tosUrl && ( 95 + <a 96 + href={tosUrl} 97 + target="_blank" 98 + rel="noopener noreferrer" 99 + className="mt-0.5 block text-xs text-primary hover:underline" 100 + > 101 + Read full Terms of Service 102 + </a> 103 + )} 104 + </div> 105 + </div> 106 + ) 107 + } 108 + 76 109 case 'custom_checkbox': 77 110 return ( 78 111 <div className="flex items-start gap-2">
+55
src/components/onboarding-modal.test.tsx
··· 196 196 expect(screen.getByRole('button', { name: /continue/i })).toBeEnabled() 197 197 }) 198 198 199 + it('renders a ToS link when config.tosUrl is set', () => { 200 + render( 201 + <OnboardingModal 202 + {...defaultProps} 203 + fields={[ 204 + makeField({ 205 + id: 'tos', 206 + fieldType: 'tos_acceptance', 207 + label: 'Accept Terms', 208 + config: { tosUrl: 'https://example.com/tos' }, 209 + }), 210 + ]} 211 + /> 212 + ) 213 + const link = screen.getByRole('link', { name: /read full terms of service/i }) 214 + expect(link).toHaveAttribute('href', 'https://example.com/tos') 215 + expect(link).toHaveAttribute('target', '_blank') 216 + expect(link).toHaveAttribute('rel', 'noopener noreferrer') 217 + }) 218 + 219 + it('does not render a link when config.tosUrl is absent', () => { 220 + render( 221 + <OnboardingModal 222 + {...defaultProps} 223 + fields={[ 224 + makeField({ 225 + id: 'tos', 226 + fieldType: 'tos_acceptance', 227 + label: 'Accept Terms', 228 + config: null, 229 + }), 230 + ]} 231 + /> 232 + ) 233 + expect(screen.queryByRole('link')).not.toBeInTheDocument() 234 + }) 235 + 236 + it('passes axe accessibility check for tos_acceptance with tosUrl', async () => { 237 + const { container } = render( 238 + <OnboardingModal 239 + {...defaultProps} 240 + fields={[ 241 + makeField({ 242 + id: 'tos', 243 + fieldType: 'tos_acceptance', 244 + label: 'Accept Terms', 245 + config: { tosUrl: 'https://example.com/tos' }, 246 + }), 247 + ]} 248 + /> 249 + ) 250 + const results = await axe(container) 251 + expect(results).toHaveNoViolations() 252 + }) 253 + 199 254 it('passes axe accessibility check', async () => { 200 255 const { container } = render(<OnboardingModal {...defaultProps} />) 201 256 const results = await axe(container)