Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS16] support live activity #1

Merged
merged 10 commits into from
Jul 25, 2023
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
5 changes: 5 additions & 0 deletions notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
68 changes: 57 additions & 11 deletions payload/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,29 @@ 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 {
content map[string]interface{}
}

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"`
Event string `json:"event,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
ContentState map[string]interface{} `json:"content-state,omitempty"`
}

type alert struct {
Expand Down Expand Up @@ -218,7 +224,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
Expand Down Expand Up @@ -378,6 +384,46 @@ 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/unnotificationinterruptionlevel/
//
// {"aps":{"stale-date":1650998941}}
func (p *Payload) StaleDate(staledate int64) *Payload {
GonzaloAvilez marked this conversation as resolved.
Show resolved Hide resolved
p.aps().StaleDate = staledate
return p
}

// Event 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)
Expand Down
36 changes: 36 additions & 0 deletions payload/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,39 @@ 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 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))
}