feat(bskyweb): match Pass Designer pass.json + assets literally

This commit is contained in:
vineyardbovines
2026-06-26 10:57:26 -04:00
parent 54106bde58
commit b98699c379
9 changed files with 108 additions and 243 deletions
+52 -71
View File
@@ -30,30 +30,32 @@ func ThemeBackgroundRGB(theme string) string {
} }
type passField struct { type passField struct {
Key string `json:"key"` Key string `json:"key"`
Label string `json:"label"` Label string `json:"label"`
Value string `json:"value"` Value string `json:"value"`
DateStyle string `json:"dateStyle,omitempty"` DateStyle string `json:"dateStyle,omitempty"`
TextAlignment string `json:"textAlignment,omitempty"`
} }
// passFields is the iOS <= 26 storeCard/generic layout: header, primary, // genericFields is the iOS <= 26 fallback layout that Pass Designer produces
// secondary, back. // in the `generic` block. Notably no backFields - Pass Designer omits that
type passFields struct { // key in this block.
type genericFields struct {
HeaderFields []passField `json:"headerFields"` HeaderFields []passField `json:"headerFields"`
PrimaryFields []passField `json:"primaryFields"` PrimaryFields []passField `json:"primaryFields"`
SecondaryFields []passField `json:"secondaryFields"` SecondaryFields []passField `json:"secondaryFields"`
BackFields []passField `json:"backFields"`
} }
// posterFields is the iOS 27 layout used by Pass Designer. Adds auxiliary // posterFields is the iOS 27+ layout in the `posterGeneric` block. All six
// and footer field slots that the classic layout does not have. // field slots present, empty arrays where not used (matches Pass Designer's
// output literally).
type posterFields struct { type posterFields struct {
AuxiliaryFields []passField `json:"auxiliaryFields"`
BackFields []passField `json:"backFields"`
FooterFields []passField `json:"footerFields"`
HeaderFields []passField `json:"headerFields"` HeaderFields []passField `json:"headerFields"`
PrimaryFields []passField `json:"primaryFields"` PrimaryFields []passField `json:"primaryFields"`
SecondaryFields []passField `json:"secondaryFields"` SecondaryFields []passField `json:"secondaryFields"`
AuxiliaryFields []passField `json:"auxiliaryFields"`
FooterFields []passField `json:"footerFields"`
BackFields []passField `json:"backFields"`
} }
type barcode struct { type barcode struct {
@@ -64,62 +66,46 @@ type barcode struct {
} }
type pkPass struct { type pkPass struct {
FormatVersion int `json:"formatVersion"` FormatVersion int `json:"formatVersion"`
PassTypeIdentifier string `json:"passTypeIdentifier"` PassTypeIdentifier string `json:"passTypeIdentifier"`
SerialNumber string `json:"serialNumber"` SerialNumber string `json:"serialNumber"`
TeamIdentifier string `json:"teamIdentifier"` TeamIdentifier string `json:"teamIdentifier"`
OrganizationName string `json:"organizationName"` OrganizationName string `json:"organizationName"`
Description string `json:"description"` Description string `json:"description"`
LogoText string `json:"logoText"` ForegroundColor string `json:"foregroundColor"`
ForegroundColor string `json:"foregroundColor"` LabelColor string `json:"labelColor"`
LabelColor string `json:"labelColor"` BackgroundColor string `json:"backgroundColor"`
BackgroundColor string `json:"backgroundColor"` Generic genericFields `json:"generic"`
Generic passFields `json:"generic"` PosterGeneric posterFields `json:"posterGeneric"`
PosterGeneric posterFields `json:"posterGeneric"` SuppressHeaderDarkening bool `json:"suppressHeaderDarkening"`
SuppressHeaderDarkening bool `json:"suppressHeaderDarkening"` UseAutomaticColors bool `json:"useAutomaticColors"`
UseAutomaticColors bool `json:"useAutomaticColors"` Barcodes []barcode `json:"barcodes"`
Barcodes []barcode `json:"barcodes"`
} }
const passTypeIdentifier = "pass.xyz.blueskyweb.app" const passTypeIdentifier = "pass.xyz.blueskyweb.app"
// BuildPassJSON builds the pkpass JSON for both the iOS <= 26 fallback layout // BuildPassJSON builds the pkpass JSON matching the Pass Designer-exported
// (`generic` block) and the iOS 27 layout (`posterGeneric` block). The two // layout literally: `generic` has handle (primary) + member-since (secondary);
// blocks describe the same fields in the layouts each iOS version expects: // `posterGeneric` adds DID as a footer field and stacks handle + member-since
// the user sees the poster layout on iOS 27+ and the legacy layout below that. // in primaryFields.
// //
// pdsHost is the hostname extracted from the user's DID document // pdsHost and displayName are currently unused - the design landed without
// serviceEndpoint (e.g. "suillus.us-west.host.bsky.network"). // them - but their plumbing is retained in callers for a possible future
// createdAt is the user's profile record creation time, used as a rough // design. The token still carries pdsHost.
// stand-in for account creation.
func BuildPassJSON(did, handle, displayName, pdsHost, theme, teamID string, createdAt time.Time) ([]byte, error) { func BuildPassJSON(did, handle, displayName, pdsHost, theme, teamID string, createdAt time.Time) ([]byte, error) {
_ = displayName
_ = pdsHost
_ = theme
theme = CoerceTheme(theme) theme = CoerceTheme(theme)
profileURL := "https://bsky.app/profile/" + handle profileURL := "https://bsky.app/profile/" + handle
atHandle := "@" + handle
memberName := displayName
if memberName == "" {
memberName = atHandle
}
if pdsHost == "" {
pdsHost = "bsky.social"
}
createdAtISO := "" createdAtISO := ""
if !createdAt.IsZero() { if !createdAt.IsZero() {
createdAtISO = createdAt.UTC().Format(time.RFC3339) createdAtISO = createdAt.UTC().Format(time.RFC3339)
} }
// avoid an unused variable when displayName is empty - we keep memberName
// computed for a possible future design, but the current layout uses
// handle directly.
_ = memberName
pdsField := passField{Key: "pds", Label: "PDS", Value: pdsHost}
handleField := passField{Key: "handle", Label: "Handle", Value: handle} handleField := passField{Key: "handle", Label: "Handle", Value: handle}
sinceField := passField{Key: "since", Label: "Member Since", Value: createdAtISO, DateStyle: "PKDateStyleShort"} sinceField := passField{Key: "since", Label: "Member Since", Value: createdAtISO, DateStyle: "PKDateStyleShort"}
backFields := []passField{ didField := passField{Key: "did", Label: "DID", Value: did, TextAlignment: "PKTextAlignmentNatural"}
{Key: "about", Label: "About", Value: "Scan the QR code to view this Bluesky profile."},
{Key: "url", Label: "Profile URL", Value: profileURL},
}
p := pkPass{ p := pkPass{
FormatVersion: 1, FormatVersion: 1,
@@ -127,29 +113,24 @@ func BuildPassJSON(did, handle, displayName, pdsHost, theme, teamID string, crea
SerialNumber: did + "-" + theme + "-v1", SerialNumber: did + "-" + theme + "-v1",
TeamIdentifier: teamID, TeamIdentifier: teamID,
OrganizationName: "Bluesky", OrganizationName: "Bluesky",
Description: "Bluesky profile - " + atHandle, Description: "Atmosphere Account",
// logoText omitted - the logo image already carries the "Bluesky" ForegroundColor: "rgb(255,255,255)",
// wordmark; setting both would render the brand name twice. LabelColor: "rgb(255,255,255)",
LogoText: "", // Matches Pass Designer's exported backgroundColor. Theme parameter
ForegroundColor: "rgb(255, 255, 255)", // kept in the signature for the URL/serial but no longer affects color.
LabelColor: "rgb(255, 255, 255)", BackgroundColor: "rgb(44,103,244)",
BackgroundColor: ThemeBackgroundRGB(theme), Generic: genericFields{
// iOS <= 26 fallback: handle as primary, since as secondary (stacked). HeaderFields: []passField{},
Generic: passFields{
HeaderFields: []passField{pdsField},
PrimaryFields: []passField{handleField}, PrimaryFields: []passField{handleField},
SecondaryFields: []passField{sinceField}, SecondaryFields: []passField{sinceField},
BackFields: backFields,
}, },
// iOS 27+: handle and since side-by-side in primaryFields, matching
// what Pass Designer exports.
PosterGeneric: posterFields{ PosterGeneric: posterFields{
HeaderFields: []passField{pdsField}, AuxiliaryFields: []passField{},
BackFields: []passField{},
FooterFields: []passField{didField},
HeaderFields: []passField{},
PrimaryFields: []passField{handleField, sinceField}, PrimaryFields: []passField{handleField, sinceField},
SecondaryFields: []passField{}, SecondaryFields: []passField{},
AuxiliaryFields: []passField{},
FooterFields: []passField{},
BackFields: backFields,
}, },
SuppressHeaderDarkening: false, SuppressHeaderDarkening: false,
UseAutomaticColors: false, UseAutomaticColors: false,
+14 -43
View File
@@ -4,19 +4,12 @@
"serialNumber": "did:plc:abc123-dawn-v1", "serialNumber": "did:plc:abc123-dawn-v1",
"teamIdentifier": "TEAMID00", "teamIdentifier": "TEAMID00",
"organizationName": "Bluesky", "organizationName": "Bluesky",
"description": "Bluesky profile - @alice.bsky.social", "description": "Atmosphere Account",
"logoText": "", "foregroundColor": "rgb(255,255,255)",
"foregroundColor": "rgb(255, 255, 255)", "labelColor": "rgb(255,255,255)",
"labelColor": "rgb(255, 255, 255)", "backgroundColor": "rgb(44,103,244)",
"backgroundColor": "rgb(255, 109, 190)",
"generic": { "generic": {
"headerFields": [ "headerFields": [],
{
"key": "pds",
"label": "PDS",
"value": "alice.host.bsky.network"
}
],
"primaryFields": [ "primaryFields": [
{ {
"key": "handle", "key": "handle",
@@ -31,28 +24,20 @@
"value": "2026-01-25T23:59:05Z", "value": "2026-01-25T23:59:05Z",
"dateStyle": "PKDateStyleShort" "dateStyle": "PKDateStyleShort"
} }
],
"backFields": [
{
"key": "about",
"label": "About",
"value": "Scan the QR code to view this Bluesky profile."
},
{
"key": "url",
"label": "Profile URL",
"value": "https://bsky.app/profile/alice.bsky.social"
}
] ]
}, },
"posterGeneric": { "posterGeneric": {
"headerFields": [ "auxiliaryFields": [],
"backFields": [],
"footerFields": [
{ {
"key": "pds", "key": "did",
"label": "PDS", "label": "DID",
"value": "alice.host.bsky.network" "value": "did:plc:abc123",
"textAlignment": "PKTextAlignmentNatural"
} }
], ],
"headerFields": [],
"primaryFields": [ "primaryFields": [
{ {
"key": "handle", "key": "handle",
@@ -66,21 +51,7 @@
"dateStyle": "PKDateStyleShort" "dateStyle": "PKDateStyleShort"
} }
], ],
"secondaryFields": [], "secondaryFields": []
"auxiliaryFields": [],
"footerFields": [],
"backFields": [
{
"key": "about",
"label": "About",
"value": "Scan the QR code to view this Bluesky profile."
},
{
"key": "url",
"label": "Profile URL",
"value": "https://bsky.app/profile/alice.bsky.social"
}
]
}, },
"suppressHeaderDarkening": false, "suppressHeaderDarkening": false,
"useAutomaticColors": false, "useAutomaticColors": false,
+14 -43
View File
@@ -4,19 +4,12 @@
"serialNumber": "did:plc:abc123-day-v1", "serialNumber": "did:plc:abc123-day-v1",
"teamIdentifier": "TEAMID00", "teamIdentifier": "TEAMID00",
"organizationName": "Bluesky", "organizationName": "Bluesky",
"description": "Bluesky profile - @alice.bsky.social", "description": "Atmosphere Account",
"logoText": "", "foregroundColor": "rgb(255,255,255)",
"foregroundColor": "rgb(255, 255, 255)", "labelColor": "rgb(255,255,255)",
"labelColor": "rgb(255, 255, 255)", "backgroundColor": "rgb(44,103,244)",
"backgroundColor": "rgb(117, 175, 255)",
"generic": { "generic": {
"headerFields": [ "headerFields": [],
{
"key": "pds",
"label": "PDS",
"value": "alice.host.bsky.network"
}
],
"primaryFields": [ "primaryFields": [
{ {
"key": "handle", "key": "handle",
@@ -31,28 +24,20 @@
"value": "2026-01-25T23:59:05Z", "value": "2026-01-25T23:59:05Z",
"dateStyle": "PKDateStyleShort" "dateStyle": "PKDateStyleShort"
} }
],
"backFields": [
{
"key": "about",
"label": "About",
"value": "Scan the QR code to view this Bluesky profile."
},
{
"key": "url",
"label": "Profile URL",
"value": "https://bsky.app/profile/alice.bsky.social"
}
] ]
}, },
"posterGeneric": { "posterGeneric": {
"headerFields": [ "auxiliaryFields": [],
"backFields": [],
"footerFields": [
{ {
"key": "pds", "key": "did",
"label": "PDS", "label": "DID",
"value": "alice.host.bsky.network" "value": "did:plc:abc123",
"textAlignment": "PKTextAlignmentNatural"
} }
], ],
"headerFields": [],
"primaryFields": [ "primaryFields": [
{ {
"key": "handle", "key": "handle",
@@ -66,21 +51,7 @@
"dateStyle": "PKDateStyleShort" "dateStyle": "PKDateStyleShort"
} }
], ],
"secondaryFields": [], "secondaryFields": []
"auxiliaryFields": [],
"footerFields": [],
"backFields": [
{
"key": "about",
"label": "About",
"value": "Scan the QR code to view this Bluesky profile."
},
{
"key": "url",
"label": "Profile URL",
"value": "https://bsky.app/profile/alice.bsky.social"
}
]
}, },
"suppressHeaderDarkening": false, "suppressHeaderDarkening": false,
"useAutomaticColors": false, "useAutomaticColors": false,
+14 -43
View File
@@ -4,19 +4,12 @@
"serialNumber": "did:plc:abc123-dusk-v1", "serialNumber": "did:plc:abc123-dusk-v1",
"teamIdentifier": "TEAMID00", "teamIdentifier": "TEAMID00",
"organizationName": "Bluesky", "organizationName": "Bluesky",
"description": "Bluesky profile - @alice.bsky.social", "description": "Atmosphere Account",
"logoText": "", "foregroundColor": "rgb(255,255,255)",
"foregroundColor": "rgb(255, 255, 255)", "labelColor": "rgb(255,255,255)",
"labelColor": "rgb(255, 255, 255)", "backgroundColor": "rgb(44,103,244)",
"backgroundColor": "rgb(177, 90, 162)",
"generic": { "generic": {
"headerFields": [ "headerFields": [],
{
"key": "pds",
"label": "PDS",
"value": "alice.host.bsky.network"
}
],
"primaryFields": [ "primaryFields": [
{ {
"key": "handle", "key": "handle",
@@ -31,28 +24,20 @@
"value": "2026-01-25T23:59:05Z", "value": "2026-01-25T23:59:05Z",
"dateStyle": "PKDateStyleShort" "dateStyle": "PKDateStyleShort"
} }
],
"backFields": [
{
"key": "about",
"label": "About",
"value": "Scan the QR code to view this Bluesky profile."
},
{
"key": "url",
"label": "Profile URL",
"value": "https://bsky.app/profile/alice.bsky.social"
}
] ]
}, },
"posterGeneric": { "posterGeneric": {
"headerFields": [ "auxiliaryFields": [],
"backFields": [],
"footerFields": [
{ {
"key": "pds", "key": "did",
"label": "PDS", "label": "DID",
"value": "alice.host.bsky.network" "value": "did:plc:abc123",
"textAlignment": "PKTextAlignmentNatural"
} }
], ],
"headerFields": [],
"primaryFields": [ "primaryFields": [
{ {
"key": "handle", "key": "handle",
@@ -66,21 +51,7 @@
"dateStyle": "PKDateStyleShort" "dateStyle": "PKDateStyleShort"
} }
], ],
"secondaryFields": [], "secondaryFields": []
"auxiliaryFields": [],
"footerFields": [],
"backFields": [
{
"key": "about",
"label": "About",
"value": "Scan the QR code to view this Bluesky profile."
},
{
"key": "url",
"label": "Profile URL",
"value": "https://bsky.app/profile/alice.bsky.social"
}
]
}, },
"suppressHeaderDarkening": false, "suppressHeaderDarkening": false,
"useAutomaticColors": false, "useAutomaticColors": false,
+14 -43
View File
@@ -4,19 +4,12 @@
"serialNumber": "did:plc:abc123-night-v1", "serialNumber": "did:plc:abc123-night-v1",
"teamIdentifier": "TEAMID00", "teamIdentifier": "TEAMID00",
"organizationName": "Bluesky", "organizationName": "Bluesky",
"description": "Bluesky profile - @alice.bsky.social", "description": "Atmosphere Account",
"logoText": "", "foregroundColor": "rgb(255,255,255)",
"foregroundColor": "rgb(255, 255, 255)", "labelColor": "rgb(255,255,255)",
"labelColor": "rgb(255, 255, 255)", "backgroundColor": "rgb(44,103,244)",
"backgroundColor": "rgb(0, 21, 51)",
"generic": { "generic": {
"headerFields": [ "headerFields": [],
{
"key": "pds",
"label": "PDS",
"value": "alice.host.bsky.network"
}
],
"primaryFields": [ "primaryFields": [
{ {
"key": "handle", "key": "handle",
@@ -31,28 +24,20 @@
"value": "2026-01-25T23:59:05Z", "value": "2026-01-25T23:59:05Z",
"dateStyle": "PKDateStyleShort" "dateStyle": "PKDateStyleShort"
} }
],
"backFields": [
{
"key": "about",
"label": "About",
"value": "Scan the QR code to view this Bluesky profile."
},
{
"key": "url",
"label": "Profile URL",
"value": "https://bsky.app/profile/alice.bsky.social"
}
] ]
}, },
"posterGeneric": { "posterGeneric": {
"headerFields": [ "auxiliaryFields": [],
"backFields": [],
"footerFields": [
{ {
"key": "pds", "key": "did",
"label": "PDS", "label": "DID",
"value": "alice.host.bsky.network" "value": "did:plc:abc123",
"textAlignment": "PKTextAlignmentNatural"
} }
], ],
"headerFields": [],
"primaryFields": [ "primaryFields": [
{ {
"key": "handle", "key": "handle",
@@ -66,21 +51,7 @@
"dateStyle": "PKDateStyleShort" "dateStyle": "PKDateStyleShort"
} }
], ],
"secondaryFields": [], "secondaryFields": []
"auxiliaryFields": [],
"footerFields": [],
"backFields": [
{
"key": "about",
"label": "About",
"value": "Scan the QR code to view this Bluesky profile."
},
{
"key": "url",
"label": "Profile URL",
"value": "https://bsky.app/profile/alice.bsky.social"
}
]
}, },
"suppressHeaderDarkening": false, "suppressHeaderDarkening": false,
"useAutomaticColors": false, "useAutomaticColors": false,
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB