Files
supabase/apps/docs/components/AppleSecretGenerator/AppleSecretGenerator.tsx
T
Danny White 1131e3e2ce fix(ui): default Button variant to default instead of primary (#50160)
## What kind of change does this PR introduce?

Bug fix / design-system alignment for the legacy `Button` from `ui`.

## What is the current behavior?

Omitting `variant` on the legacy `Button` falls back to brand-green
`primary`. That makes accidental greens easy, and it is hard to spot the
real main action on busy pages.

## What is the new behavior?

- Legacy `Button` now defaults to neutral `default`
- Intentional primary CTAs (create, save, submit, marketing CTAs, and
matching `ButtonTooltip` usages) now set `variant="primary"` so their
appearance is unchanged
- Neutral actions that previously relied on the old fallback (cancel,
close, back, dashboard nav, and similar) become grey/white
- Design-system docs updated; regression tests cover the new default

`Button_Shadcn_` is unchanged. It already uses its own CVA default.

This is PR 1 of 2 in a stack. PR 2 drops now-redundant
`variant="default"` props.

## To test

Studio (http://localhost:8082):

- `/sign-in`: Sign in stays green
- Open a project → Database → Tables: New table stays green
- Auth → Users → Invite: Invite user stays green; Cancel / dismiss
controls stay neutral
- Project Settings → General: edit a field so Cancel and Save appear.
Cancel is neutral, Save is green

Design system (http://localhost:3003):

- Components → Button: default demo is neutral; primary demo is green;
featured preview is the default variant

Marketing (optional):

- www header: Start your project stays green; logged-in Dashboard is
neutral

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Style**
- Buttons now default to a neutral style, while primary actions across
Studio, documentation, marketing pages, forms, dialogs, and error states
use prominent primary styling.
- Updated button examples and previews clarify the distinction between
default and primary variants.
  - Event registration now includes a directional arrow icon.

- **Tests**
- Added coverage confirming default button styling and explicit primary
styling behave as expected.
- Updated related test fixtures to use primary styling where
appropriate.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-10 11:23:17 +10:00

212 lines
5.7 KiB
TypeScript

import { useState } from 'react'
import { Button, Input } from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { Input as DataInput } from 'ui-patterns/DataInputs/Input'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
function base64URL(value: string) {
return globalThis.btoa(value).replace(/[=]/g, '').replace(/[+]/g, '-').replace(/[\/]/g, '_')
}
/*
Convert a string into an ArrayBuffer
from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
*/
function stringToArrayBuffer(value: string) {
const buf = new ArrayBuffer(value.length)
const bufView = new Uint8Array(buf)
for (let i = 0; i < value.length; i++) {
bufView[i] = value.charCodeAt(i)
}
return buf
}
function arrayBufferToString(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf))
}
const generateAppleSecretKey = async (
kid: string,
iss: string,
sub: string,
file: File
): Promise<{ kid: string; jwt: string; exp: number }> => {
if (!kid) {
const match = file.name.match(/AuthKey_([^.]+)[.].*$/i)
if (match && match[1]) {
kid = match[1]
}
}
if (!kid) {
throw new Error(
`No Key ID provided. The file "${file.name}" does not follow the AuthKey_XXXXXXXXXX.p8 pattern. Please provide a Key ID manually.`
)
}
const contents = await file.text()
if (!contents.match(/^\s*-+BEGIN PRIVATE KEY-+[^-]+-+END PRIVATE KEY-+\s*$/i)) {
throw new Error(`Chosen file does not appear to be a PEM encoded PKCS8 private key file.`)
}
// remove PEM headers and spaces
const pkcs8 = stringToArrayBuffer(
globalThis.atob(contents.replace(/-+[^-]+-+/g, '').replace(/\s+/g, ''))
)
const privateKey = await globalThis.crypto.subtle.importKey(
'pkcs8',
pkcs8,
{
name: 'ECDSA',
namedCurve: 'P-256',
},
true,
['sign']
)
const iat = Math.floor(Date.now() / 1000)
const exp = iat + 180 * 24 * 60 * 60
const jwt = [
base64URL(JSON.stringify({ typ: 'JWT', kid, alg: 'ES256' })),
base64URL(
JSON.stringify({
iss,
sub,
iat,
exp,
aud: 'https://appleid.apple.com',
})
),
]
const signature = await globalThis.crypto.subtle.sign(
{
name: 'ECDSA',
hash: 'SHA-256',
},
privateKey,
stringToArrayBuffer(jwt.join('.'))
)
jwt.push(base64URL(arrayBufferToString(signature)))
return { kid, jwt: jwt.join('.'), exp }
}
const AppleSecretGenerator = () => {
const [file, setFile] = useState({ file: null as File | null })
const [teamID, setTeamID] = useState('')
const [serviceID, setServiceID] = useState('')
const [keyID, setKeyID] = useState('')
const [secretKey, setSecretKey] = useState('')
const [expiresAt, setExpiresAt] = useState('')
const [error, setError] = useState('')
return (
<div className="flex flex-col gap-3">
<FormItemLayout
isReactForm={false}
layout="vertical"
label="Account ID"
description="Found in the upper-right corner of Apple Developer Center."
labelOptional="required"
>
<Input
placeholder="Apple Developer account ID, 10 alphanumeric digits"
value={teamID}
onChange={(e) => setTeamID(e.target.value.trim())}
/>
</FormItemLayout>
<FormItemLayout
isReactForm={false}
layout="vertical"
label="Service ID"
description="Found under Certificates, Identifiers & Profiles in Apple Developer Center."
labelOptional="required"
>
<Input
placeholder="ID of the service, example: com.example.app.service"
value={serviceID}
onChange={(e) => setServiceID(e.target.value.trim())}
/>
</FormItemLayout>
<FormItemLayout
isReactForm={false}
layout="vertical"
label="Key ID"
description="If the file you select does not preserve the original name from Apple Developer Center, please enter the key ID."
labelOptional="(optional)"
>
<Input
placeholder="Extracted from filename, AuthKey_XXXXXXXXXX.p8"
value={keyID}
onChange={(e) => setKeyID(e.target.value.trim())}
/>
</FormItemLayout>
<div>
<input
type="file"
onChange={(e) => {
setFile({ file: e.target.files?.[0] || null })
}}
/>
</div>
<div style={{ height: '1rem' }} />
<Button
variant="primary"
size="medium"
disabled={
!(
teamID.length === 10 &&
serviceID &&
((globalThis && globalThis.showOpenFilePicker) || file.file)
)
}
onClick={async () => {
setError('')
try {
const { kid, jwt, exp } = await generateAppleSecretKey(
keyID,
teamID,
serviceID,
file.file!
)
setKeyID(kid)
setSecretKey(jwt)
setExpiresAt(new Date(exp * 1000).toString())
setError('')
} catch (e: any) {
setError(e.message)
console.error(e)
}
}}
>
Generate Secret Key
</Button>
{error && <Admonition type="danger">{error}</Admonition>}
{secretKey && (
<>
<div style={{ height: '1rem' }} />
<FormItemLayout
isReactForm={false}
layout="vertical"
label="Secret Key"
description={`Valid until: ${expiresAt}. Make sure you generate a new one before then!`}
>
<DataInput value={secretKey} copy reveal />
</FormItemLayout>
</>
)}
</div>
)
}
export default AppleSecretGenerator