From 79519b77e86557efdbedf71d46d8d1b9553c1ca0 Mon Sep 17 00:00:00 2001 From: Ian Leue Date: Thu, 8 Aug 2024 04:54:53 -0400 Subject: [PATCH] Live Activity support added (revised and expanded) (#219) * Merge pull request #6 from braze-inc/CH-4323 * [CH-4323] Add Live Activity Support * [CH-5411] Add support for Attributes/AttributesType for push-to-start live activities (#7) --- client_test.go | 11 +++++ notification.go | 8 ++++ payload/builder.go | 103 +++++++++++++++++++++++++++++++++++----- payload/builder_test.go | 61 ++++++++++++++++++++++++ 4 files changed, 172 insertions(+), 11 deletions(-) diff --git a/client_test.go b/client_test.go index 83061ddb..cca1349f 100644 --- a/client_test.go +++ b/client_test.go @@ -322,6 +322,17 @@ func TestPushTypeMDMHeader(t *testing.T) { assert.NoError(t, err) } +func TestPushTypeLiveActivityHeader(t *testing.T) { + n := mockNotification() + n.PushType = apns.PushTypeLiveActivity + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "liveactivity", r.Header.Get("apns-push-type")) + })) + defer server.Close() + _, err := mockClient(server.URL).Push(n) + assert.NoError(t, err) +} + func TestAuthorizationHeader(t *testing.T) { n := mockNotification() token := mockToken() diff --git a/notification.go b/notification.go index 69bf312d..5cbc921f 100644 --- a/notification.go +++ b/notification.go @@ -63,6 +63,14 @@ const ( // contact the MDM server. If you set this push type, you must use the topic // from the UID attribute in the subject of your MDM push certificate. PushTypeMDM EPushType = "mdm" + + // PushTypeLiveActivity is used for Live Activities that display various + // real-time information. If you set this push type, the topic field must + // use your app’s bundle ID with `push-type.liveactivity` appended to the end. + // The live activity push supports only token-based authentication. This + // push type is recommended for iOS. It is not available on macOS, tvOS, + // watchOS and iPadOS. + PushTypeLiveActivity EPushType = "liveactivity" ) const ( diff --git a/payload/builder.go b/payload/builder.go index a2ff30da..7d0cd5ac 100644 --- a/payload/builder.go +++ b/payload/builder.go @@ -23,6 +23,17 @@ const ( InterruptionLevelCritical EInterruptionLevel = "critical" ) +// LiveActivityEvent defines the value for the payload aps event +type ELiveActivityEvent string + +const ( + // LiveActivityEventUpdate is used to update an live activity. + LiveActivityEventUpdate ELiveActivityEvent = "update" + + // LiveActivityEventEnd is used to end an live activity. + LiveActivityEventEnd ELiveActivityEvent = "end" +) + // Payload represents a notification which holds the content that will be // marshalled as JSON. type Payload struct { @@ -30,16 +41,23 @@ type Payload struct { } type aps struct { - Alert interface{} `json:"alert,omitempty"` - Badge interface{} `json:"badge,omitempty"` - Category string `json:"category,omitempty"` - ContentAvailable int `json:"content-available,omitempty"` - InterruptionLevel EInterruptionLevel `json:"interruption-level,omitempty"` - MutableContent int `json:"mutable-content,omitempty"` - RelevanceScore interface{} `json:"relevance-score,omitempty"` - Sound interface{} `json:"sound,omitempty"` - ThreadID string `json:"thread-id,omitempty"` - URLArgs []string `json:"url-args,omitempty"` + Alert interface{} `json:"alert,omitempty"` + Badge interface{} `json:"badge,omitempty"` + Category string `json:"category,omitempty"` + ContentAvailable int `json:"content-available,omitempty"` + InterruptionLevel EInterruptionLevel `json:"interruption-level,omitempty"` + MutableContent int `json:"mutable-content,omitempty"` + RelevanceScore interface{} `json:"relevance-score,omitempty"` + Sound interface{} `json:"sound,omitempty"` + ThreadID string `json:"thread-id,omitempty"` + URLArgs []string `json:"url-args,omitempty"` + ContentState map[string]interface{} `json:"content-state,omitempty"` + DismissalDate int64 `json:"dismissal-date,omitempty"` + StaleDate int64 `json:"stale-date,omitempty"` + Event ELiveActivityEvent `json:"event,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + AttributesType string `json:"attributes-type,omitempty"` + Attributes map[string]interface{} `json:"attributes,omitempty"` } type alert struct { @@ -81,6 +99,69 @@ func (p *Payload) Alert(alert interface{}) *Payload { return p } +// SetContentState sets the aps content-state on the payload. +// This will update content-state of live activity widget. +// +// {"aps":{"content-state": {} }}` +func (p *Payload) SetContentState(contentState map[string]interface{}) *Payload { + p.aps().ContentState = contentState + return p +} + +// SetDismissalDate sets the aps dismissal-date on the payload. +// This will remove the live activity from the user's UI at the given timestamp. +// +// {"aps":{"dismissal-date": DismissalDate }}` +func (p *Payload) SetDismissalDate(dismissalDate int64) *Payload { + p.aps().DismissalDate = dismissalDate + return p +} + +// SetStaleDate sets the aps stale-date on the payload. +// This will mark this live activity update as outdated at the given timestamp. +// +// {"aps":{"stale-date": StaleDate }}` +func (p *Payload) SetStaleDate(staleDate int64) *Payload { + p.aps().StaleDate = staleDate + return p +} + +// SetEvent sets the aps event type on the payload. +// This can either be `LiveActivityEventUpdate` or `LiveActivityEventEnd` +// +// {"aps":{"event": Event }}` +func (p *Payload) SetEvent(event ELiveActivityEvent) *Payload { + p.aps().Event = event + return p +} + +// SetTimestamp sets the aps timestamp on the payload. +// This will let live activity know when to update the stuff. +// +// {"aps":{"timestamp": Timestamp }}` +func (p *Payload) SetTimestamp(timestamp int64) *Payload { + p.aps().Timestamp = timestamp + return p +} + +// SetAttributesType sets the aps attributes-type field on the payload. +// This is used for push-to-start live activities +// +// {"aps":{"attributes-type": attributesType }}` +func (p *Payload) SetAttributesType(attributesType string) *Payload { + p.aps().AttributesType = attributesType + return p +} + +// SetAttributes sets the aps attributes field on the payload. +// This is used for push-to-start live activities +// +// {"aps":{"attributes": attributes }}` +func (p *Payload) SetAttributes(attributes map[string]interface{}) *Payload { + p.aps().Attributes = attributes + return p +} + // Badge sets the aps badge on the payload. // This will display a numeric badge on the app icon. // @@ -218,7 +299,7 @@ func (p *Payload) AlertLaunchImage(image string) *Payload { // specifiers in loc-key. See Localized Formatted Strings in Apple // documentation for more information. // -// {"aps":{"alert":{"loc-args":args}}} +// {"aps":{"alert":{"loc-args":args}}} func (p *Payload) AlertLocArgs(args []string) *Payload { p.aps().alert().LocArgs = args return p diff --git a/payload/builder_test.go b/payload/builder_test.go index a9650206..b0351d06 100644 --- a/payload/builder_test.go +++ b/payload/builder_test.go @@ -3,6 +3,7 @@ package payload_test import ( "encoding/json" "testing" + "time" . "github.com/sideshow/apns2/payload" "github.com/stretchr/testify/assert" @@ -146,6 +147,66 @@ func TestCategory(t *testing.T) { assert.Equal(t, `{"aps":{"category":"NEW_MESSAGE_CATEGORY"}}`, string(b)) } +func TestContentState(t *testing.T) { + payload := NewPayload().SetContentState(map[string]interface{}{"my_int": 13, "my_string": "foo"}) + b, _ := json.Marshal(payload) + assert.Equal(t, `{"aps":{"content-state":{"my_int":13,"my_string":"foo"}}}`, string(b)) +} + +func TestDismissalDate(t *testing.T) { + timestamp := time.Date(2023, 1, 27, 12, 14, 00, 00, time.UTC).Unix() + payload := NewPayload().SetDismissalDate(timestamp) + b, _ := json.Marshal(payload) + assert.Equal(t, `{"aps":{"dismissal-date":1674821640}}`, string(b)) +} + +func TestStaleDate(t *testing.T) { + timestamp := time.Date(2023, 1, 27, 12, 14, 00, 00, time.UTC).Unix() + payload := NewPayload().SetStaleDate(timestamp) + b, _ := json.Marshal(payload) + assert.Equal(t, `{"aps":{"stale-date":1674821640}}`, string(b)) +} + +func TestEventEnd(t *testing.T) { + payload := NewPayload().SetEvent(LiveActivityEventEnd) + b, _ := json.Marshal(payload) + assert.Equal(t, `{"aps":{"event":"end"}}`, string(b)) +} + +func TestEventUpdate(t *testing.T) { + payload := NewPayload().SetEvent(LiveActivityEventUpdate) + b, _ := json.Marshal(payload) + assert.Equal(t, `{"aps":{"event":"update"}}`, string(b)) +} + +func TestTimestamp(t *testing.T) { + timestamp := time.Date(2023, 1, 27, 12, 14, 00, 00, time.UTC).Unix() + payload := NewPayload().SetTimestamp(timestamp) + b, _ := json.Marshal(payload) + assert.Equal(t, `{"aps":{"timestamp":1674821640}}`, string(b)) +} + +func TestAttributesType(t *testing.T) { + attributesType := "AdventureAttributes" + payload := NewPayload().SetAttributesType(attributesType) + b, _ := json.Marshal(payload) + assert.Equal(t, `{"aps":{"attributes-type":"AdventureAttributes"}}`, string(b)) +} + +func TestAttributes(t *testing.T) { + attributes := map[string]interface{}{ + "currentHealthLevel": 100, + "eventDescription": "Adventure has begun!", + } + payload := NewPayload().SetAttributes(attributes) + b, _ := json.Marshal(payload) + assert.Equal( + t, + `{"aps":{"attributes":{"currentHealthLevel":100,"eventDescription":"Adventure has begun!"}}}`, + string(b), + ) +} + func TestMdm(t *testing.T) { payload := NewPayload().Mdm("996ac527-9993-4a0a-8528-60b2b3c2f52b") b, _ := json.Marshal(payload)