diff --git a/client.go b/client.go index 832b4210..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 @@ -224,6 +230,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..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) { @@ -169,6 +181,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 +277,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..6a83babd 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-token field on the payload. +// This is used for push-to-start live activities for channels., +// +// {"aps":{"input-push-token": 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)