diff --git a/README.md b/README.md index 32e04190..b4b5b55d 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ APNS/2 is a go package designed for simple, flexible and fast Apple Push Notific - Supports new Apple Token Based Authentication (JWT) - Supports new iOS 10 features such as Collapse IDs, Subtitles and Mutable Notifications - Supports new iOS 15 features interruptionLevel and relevanceScore +- Supports iOS 16 features for live-activity notifications - Supports persistent connections to APNs - Supports VoIP/PushKit notifications (iOS 8 and later) - Modular & easy to use diff --git a/client_test.go b/client_test.go index 83061ddb..d57ab715 100644 --- a/client_test.go +++ b/client_test.go @@ -322,6 +322,18 @@ func TestPushTypeMDMHeader(t *testing.T) { assert.NoError(t, err) } +func TestPushTypeLiveActivityHeader(t *testing.T) { + notification := mockNotification() + notification.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(notification) + assert.NoError(t, err) +} + func TestAuthorizationHeader(t *testing.T) { n := mockNotification() token := mockToken() diff --git a/notification.go b/notification.go index 69bf312d..b465ee4d 100644 --- a/notification.go +++ b/notification.go @@ -63,6 +63,11 @@ 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 to signal changes to a live activity session. + // The liveactivity push type isn’t available on watchOS, macOS, and tvOS. + // It’s recommended on iOS and iPadOS. + PushTypeLiveActivity EPushType = "liveactivity" ) const ( diff --git a/payload/builder.go b/payload/builder.go index a2ff30da..d356c2a8 100644 --- a/payload/builder.go +++ b/payload/builder.go @@ -23,6 +23,8 @@ const ( InterruptionLevelCritical EInterruptionLevel = "critical" ) +type D map[string]interface{} + // Payload represents a notification which holds the content that will be // marshalled as JSON. type Payload struct { @@ -30,16 +32,21 @@ 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"` + StaleDate int64 `json:"stale-date,omitempty"` + DismissalDate int64 `json:"dismissal-date,omitempty"` + Event string `json:"event,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + ContentState map[string]interface{} `json:"content-state,omitempty"` } type alert struct { @@ -218,7 +225,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 @@ -378,6 +385,56 @@ func (p *Payload) UnsetRelevanceScore() *Payload { return p } +// StaleDate defines the value stale-date for the aps payload +// The date when the system considers an update to the Live Activity to be out of date. +// ref: https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification +// +// {"aps":{"stale-date":1650998941}} +func (p *Payload) StaleDate(staleDate int64) *Payload { + p.aps().StaleDate = staleDate + return p +} + +// DismissalDate defines the value dismissal-date for the aps payload +// The UNIX timestamp that represents the date at which the system ends a Live Activity and removes it from the Dynamic Island and the Lock Screen +// ref: https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification +// +// {"aps":{"dismissal-date":1650998945}} +func (p *Payload) DismissalDate(dismissalDate int64) *Payload { + p.aps().DismissalDate = dismissalDate + return p +} + +// Events defines the value event for the aps payload +// Describes whether you update or end an ongoing Live Activity +// ref: https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification +// +// {"aps":{"event":"update"}} +func (p *Payload) Event(event string) *Payload { + p.aps().Event = event + return p +} + +// Timestamp defines the value timestamp for the aps payload +// The UNIX timestamp that marks the time when you send the remote notification that updates or ends a Live Activity +// ref: https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification +// +// {"aps":{"timestamp":1168364460}} +func (p *Payload) Timestamp(value int64) *Payload { + p.aps().Timestamp = value + return p +} + +// ContentState defines the value content-state for aps payload +// Describes and contains the dynamic content of a Live Activity. +// ref :https://developer.apple.com/documentation/activitykit/activity/contentstate-swift.typealias +// +// {"aps":{"content-state":{"product_id": 123456, "product_name": "nameTest", "product_quantity": 4, "delivery_time": 34}}} +func (p *Payload) ContentState(content map[string]interface{}) *Payload { + p.aps().ContentState = content + return p +} + // MarshalJSON returns the JSON encoded version of the Payload func (p *Payload) MarshalJSON() ([]byte, error) { return json.Marshal(p.content) diff --git a/payload/builder_test.go b/payload/builder_test.go index a9650206..33b3b1d9 100644 --- a/payload/builder_test.go +++ b/payload/builder_test.go @@ -235,3 +235,45 @@ func TestCombined(t *testing.T) { b, _ := json.Marshal(payload) assert.Equal(t, `{"aps":{"alert":"hello","badge":1,"interruption-level":"active","relevance-score":0.1,"sound":"Default.caf"},"key":"val"}`, string(b)) } + +func TestEvent(t *testing.T) { + payload := NewPayload().Event("update") + data, _ := json.Marshal(payload) + assert.Equal(t, `{"aps":{"event":"update"}}`, string(data)) +} + +func TestStaleDate(t *testing.T) { + payload := NewPayload().StaleDate(12324243) + data, _ := json.Marshal(payload) + assert.Equal(t, `{"aps":{"stale-date":12324243}}`, string(data)) +} + +func TestDismissalDate(t *testing.T) { + payload := NewPayload().DismissalDate(1689811132) + data, _ := json.Marshal(payload) + assert.Equal(t, `{"aps":{"dismissal-date":1689811132}}`, string(data)) +} + +func TestTimestamp(t *testing.T) { + payload := NewPayload().Timestamp(1168364460) + data, _ := json.Marshal(payload) + assert.Equal(t, `{"aps":{"timestamp":1168364460}}`, string(data)) +} + +func TestContentState(t *testing.T) { + payload := NewPayload().ContentState(D{"item_id": 3, "availability": 1, "volume": 4.5, "item_status": "ACCEPTED"}) + data, _ := json.Marshal(payload) + assert.Equal(t, `{"aps":{"content-state":{"availability":1,"item_id":3,"item_status":"ACCEPTED","volume":4.5}}}`, string(data)) +} + +func TestLiveActivityAttributes(t *testing.T) { + payload := NewPayload().Event("update").Timestamp(1168364460).StaleDate(12324243).ContentState(D{"item_id": 3, "availability": 1, "volume": 4.5, "item_status": "ACCEPTED"}) + data, _ := json.Marshal(payload) + assert.Equal(t, `{"aps":{"stale-date":12324243,"event":"update","timestamp":1168364460,"content-state":{"availability":1,"item_id":3,"item_status":"ACCEPTED","volume":4.5}}}`, string(data)) +} + +func TestLiveActivityAttributesMixedWithAlert(t *testing.T) { + payload := NewPayload().Alert("hello").Badge(1).Sound("Default.caf").InterruptionLevel(InterruptionLevelActive).RelevanceScore(0.1).Event("update").Timestamp(1168364460).StaleDate(12324243).ContentState(D{"item_id": 3, "availability": 1, "volume": 4.5, "item_status": "ACCEPTED"}) + data, _ := json.Marshal(payload) + assert.Equal(t, `{"aps":{"alert":"hello","badge":1,"interruption-level":"active","relevance-score":0.1,"sound":"Default.caf","stale-date":12324243,"event":"update","timestamp":1168364460,"content-state":{"availability":1,"item_id":3,"item_status":"ACCEPTED","volume":4.5}}}`, string(data)) +}