From 55a2b9d19fd71abbefeb113e12fc5380e1cba526 Mon Sep 17 00:00:00 2001 From: James Dearlove Date: Tue, 14 Jan 2025 22:17:03 +1000 Subject: [PATCH 1/3] Add live activity channel functionality --- client.go | 3 +++ client_test.go | 12 ++++++++++++ notification.go | 5 +++++ payload/builder.go | 23 +++++++++++++++++++++++ payload/builder_test.go | 13 +++++++++++++ 5 files changed, 56 insertions(+) diff --git a/client.go b/client.go index 832b4210..56dc91b1 100644 --- a/client.go +++ b/client.go @@ -224,6 +224,9 @@ func setHeaders(r *http.Request, n *Notification) { if n.CollapseID != "" { r.Header.Set("apns-collapse-id", n.CollapseID) } + if n.ChannelID != "" { + r.Header.Set("apns-channel-id", n.ChannelID) + } if n.Priority > 0 { r.Header.Set("apns-priority", strconv.Itoa(n.Priority)) } diff --git a/client_test.go b/client_test.go index 9c83d781..97bd69bd 100644 --- a/client_test.go +++ b/client_test.go @@ -169,6 +169,7 @@ func TestDefaultHeaders(t *testing.T) { assert.Equal(t, "", r.Header.Get("apns-priority")) assert.Equal(t, "", r.Header.Get("apns-topic")) assert.Equal(t, "", r.Header.Get("apns-expiration")) + assert.Equal(t, "", r.Header.Get("apns-channel-id")) assert.Equal(t, "", r.Header.Get("thread-id")) assert.Equal(t, "alert", r.Header.Get("apns-push-type")) })) @@ -264,6 +265,17 @@ func TestExpirationHeader(t *testing.T) { assert.NoError(t, err) } +func TestChannelHeader(t *testing.T) { + n := mockNotification() + n.ChannelID = "channel123" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "channel123", r.Header.Get("apns-channel-id")) + })) + defer server.Close() + _, err := mockClient(server.URL).Push(n) + assert.NoError(t, err) +} + func TestPushTypeAlertHeader(t *testing.T) { n := mockNotification() n.PushType = apns.PushTypeAlert diff --git a/notification.go b/notification.go index 54c8cafd..7d6cd3c4 100644 --- a/notification.go +++ b/notification.go @@ -116,6 +116,11 @@ type Notification struct { // device. DeviceToken string + // A base64-encoded string that identifies which channel to publish a + // payload to. This string is generated by APNs, using either the channel + // management API or the CloudKit console. + ChannelID string + // The topic of the remote notification, which is typically the bundle ID // for your app. The certificate you create in the Apple Developer Member // Center must include the capability for this topic. If your certificate diff --git a/payload/builder.go b/payload/builder.go index dbda145c..054a4271 100644 --- a/payload/builder.go +++ b/payload/builder.go @@ -27,6 +27,9 @@ const ( type ELiveActivityEvent string const ( + // LiveActivityEventStart is used to start an live activity. + LiveActivityEventStart ELiveActivityEvent = "start" + // LiveActivityEventUpdate is used to update an live activity. LiveActivityEventUpdate ELiveActivityEvent = "update" @@ -58,6 +61,8 @@ type aps struct { Timestamp int64 `json:"timestamp,omitempty"` AttributesType string `json:"attributes-type,omitempty"` Attributes map[string]interface{} `json:"attributes,omitempty"` + InputPushChannel string `json:"input-push-channel,omitempty"` + InputPushToken int `json:"input-push-token,omitempty"` } type alert struct { @@ -164,6 +169,24 @@ func (p *Payload) SetAttributes(attributes map[string]interface{}) *Payload { return p } +// SetInputPushChannel sets the aps input-push-channel field on the payload. +// This is used for push-to-start live activities for channels. +// +// {"aps":{"input-push-channel": channelID }}` +func (p *Payload) SetInputPushChannel(channelID string) *Payload { + p.aps().InputPushChannel = channelID + return p +} + +// SetInputPushToken sets the aps input-push-channel field on the payload. +// This is used for push-to-start live activities for channels. +// +// {"aps":{"input-push-channel": channelID }}` +func (p *Payload) SetInputPushToken(token int) *Payload { + p.aps().InputPushToken = token + return p +} + // Badge sets the aps badge on the payload. // This will display a numeric badge on the app icon. // diff --git a/payload/builder_test.go b/payload/builder_test.go index cfde3817..5722ae1c 100644 --- a/payload/builder_test.go +++ b/payload/builder_test.go @@ -219,6 +219,19 @@ func TestAttributes(t *testing.T) { ) } +func TestInputPushChannel(t *testing.T) { + channelID := "channelid123" + payload := NewPayload().SetInputPushChannel(channelID) + b, _ := json.Marshal(payload) + assert.Equal(t, `{"aps":{"input-push-channel":"channelid123"}}`, string(b)) +} + +func TestInputPushToken(t *testing.T) { + payload := NewPayload().SetInputPushToken(1) + b, _ := json.Marshal(payload) + assert.Equal(t, `{"aps":{"input-push-token":1}}`, string(b)) +} + func TestMdm(t *testing.T) { payload := NewPayload().Mdm("996ac527-9993-4a0a-8528-60b2b3c2f52b") b, _ := json.Marshal(payload) From f4fcddee9aef9028ab3a920832caa11b5cc0d628 Mon Sep 17 00:00:00 2001 From: James Dearlove Date: Tue, 14 Jan 2025 22:50:51 +1000 Subject: [PATCH 2/3] If ChannelID is defined, use the broadcast endpoint --- client.go | 8 +++++++- client_test.go | 12 ++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/client.go b/client.go index 56dc91b1..10315cc0 100644 --- a/client.go +++ b/client.go @@ -171,7 +171,13 @@ func (c *Client) PushWithContext(ctx Context, n *Notification) (*Response, error return nil, err } - url := c.Host + "/3/device/" + n.DeviceToken + url := c.Host + if n.ChannelID != "" { + url = url + "/4/broadcasts/apps/" + n.Topic + } else { + url = url + "/3/device/" + n.DeviceToken + } + request, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload)) if err != nil { return nil, err diff --git a/client_test.go b/client_test.go index 97bd69bd..5b8a959d 100644 --- a/client_test.go +++ b/client_test.go @@ -160,6 +160,18 @@ func TestURL(t *testing.T) { assert.NoError(t, err) } +func TestBroadcastURL(t *testing.T) { + n := mockNotification() + n.ChannelID = "channel123" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, fmt.Sprintf("/4/broadcasts/apps/%s", n.Topic), r.URL.String()) + })) + defer server.Close() + _, err := mockClient(server.URL).Push(n) + assert.NoError(t, err) +} + func TestDefaultHeaders(t *testing.T) { n := mockNotification() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { From 67c84f6b2a4e2e4a9e43eb5660ebd091eb01735b Mon Sep 17 00:00:00 2001 From: James Dearlove Date: Sun, 2 Feb 2025 22:48:28 +1000 Subject: [PATCH 3/3] Typo on docs --- payload/builder.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/payload/builder.go b/payload/builder.go index 054a4271..6a83babd 100644 --- a/payload/builder.go +++ b/payload/builder.go @@ -178,10 +178,10 @@ func (p *Payload) SetInputPushChannel(channelID string) *Payload { return p } -// SetInputPushToken sets the aps input-push-channel field on the payload. -// This is used for push-to-start live activities for channels. +// SetInputPushToken sets the aps input-push-token field on the payload. +// This is used for push-to-start live activities for channels., // -// {"aps":{"input-push-channel": channelID }}` +// {"aps":{"input-push-token": channelID }}` func (p *Payload) SetInputPushToken(token int) *Payload { p.aps().InputPushToken = token return p