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