From 79fa4dbcf1d0bb6c7de9e96aede5a2534c07c3d0 Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Sun, 4 Aug 2024 20:59:56 +1200 Subject: [PATCH 1/8] Add go versions 1.19 1.20 1.21 1.22 (#228) * Add go versions 1.19 1.20 1.21 1.22 * Ignore the apns2 cli in coverage tests --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fa456ff5..7b92f4c5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - go: [1.16.x, 1.17.x, 1.18.x] + go: [1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x, 1.21.x, 1.22.x] steps: - uses: actions/checkout@v2 @@ -33,4 +33,4 @@ jobs: - name: Update coverage env: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: goveralls -coverprofile=covprofile -service=github + run: goveralls -ignore=apns2/main.go -coverprofile=covprofile -service=github From 79519b77e86557efdbedf71d46d8d1b9553c1ca0 Mon Sep 17 00:00:00 2001 From: Ian Leue Date: Thu, 8 Aug 2024 04:54:53 -0400 Subject: [PATCH 2/8] 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) From a0adba5e0e50dd1655b2f7bf5cd8ca0433901893 Mon Sep 17 00:00:00 2001 From: Julia Rzhevskaia Date: Thu, 8 Aug 2024 10:59:37 +0200 Subject: [PATCH 3/8] add subtitle-loc-key and subtitle-loc-args in payload alert (#218) --- payload/builder.go | 24 ++++++++++++++++++++++++ payload/builder_test.go | 12 ++++++++++++ 2 files changed, 36 insertions(+) diff --git a/payload/builder.go b/payload/builder.go index 7d0cd5ac..dbda145c 100644 --- a/payload/builder.go +++ b/payload/builder.go @@ -71,6 +71,8 @@ type alert struct { Subtitle string `json:"subtitle,omitempty"` TitleLocArgs []string `json:"title-loc-args,omitempty"` TitleLocKey string `json:"title-loc-key,omitempty"` + SubtitleLocArgs []string `json:"subtitle-loc-args,omitempty"` + SubtitleLocKey string `json:"subtitle-loc-key,omitempty"` SummaryArg string `json:"summary-arg,omitempty"` SummaryArgCount int `json:"summary-arg-count,omitempty"` } @@ -274,6 +276,28 @@ func (p *Payload) AlertSubtitle(subtitle string) *Payload { return p } +// AlertSubtitleLocKey sets the aps alert subtitle localization key on the payload. +// This is the key to a subtitle string in the Localizable.strings file for the +// current localization. See Localized Formatted Strings in Apple documentation +// for more information. +// +// {"aps":{"alert":{"subtitle-loc-key":key}}} +func (p *Payload) AlertSubtitleLocKey(key string) *Payload { + p.aps().alert().SubtitleLocKey = key + return p +} + +// AlertSubtitleLocArgs sets the aps alert subtitle localization args on the payload. +// These are the variable string values to appear in place of the format +// specifiers in subtitle-loc-key. See Localized Formatted Strings in Apple +// documentation for more information. +// +// {"aps":{"alert":{"title-loc-args":args}}} +func (p *Payload) AlertSubtitleLocArgs(args []string) *Payload { + p.aps().alert().SubtitleLocArgs = args + return p +} + // AlertBody sets the aps alert body on the payload. // This is the text of the alert message. // diff --git a/payload/builder_test.go b/payload/builder_test.go index b0351d06..cfde3817 100644 --- a/payload/builder_test.go +++ b/payload/builder_test.go @@ -111,6 +111,18 @@ func TestAlertSubtitle(t *testing.T) { assert.Equal(t, `{"aps":{"alert":{"subtitle":"hello"}}}`, string(b)) } +func TestAlertSubtitleLocKey(t *testing.T) { + payload := NewPayload().AlertSubtitleLocKey("Notification.Key.TestSubtitle") + b, _ := json.Marshal(payload) + assert.Equal(t, `{"aps":{"alert":{"subtitle-loc-key":"Notification.Key.TestSubtitle"}}}`, string(b)) +} + +func TestAlertSubtitleLocArgs(t *testing.T) { + payload := NewPayload().AlertSubtitleLocArgs([]string{"one", "two"}) + b, _ := json.Marshal(payload) + assert.Equal(t, `{"aps":{"alert":{"subtitle-loc-args":["one","two"]}}}`, string(b)) +} + func TestAlertBody(t *testing.T) { payload := NewPayload().AlertBody("body") b, _ := json.Marshal(payload) From 6572a53bd811b0159fcec85abcc182ceebdbccd1 Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Sun, 11 Aug 2024 14:14:24 +1200 Subject: [PATCH 4/8] Update ioutil.ReadFile to os.ReadFile (#230) --- certificate/certificate.go | 6 +++--- certificate/certificate_test.go | 8 ++++---- token/token.go | 4 ++-- token/token_test.go | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/certificate/certificate.go b/certificate/certificate.go index 6496720a..7ed6590d 100644 --- a/certificate/certificate.go +++ b/certificate/certificate.go @@ -8,7 +8,7 @@ import ( "crypto/x509" "encoding/pem" "errors" - "io/ioutil" + "os" "strings" "golang.org/x/crypto/pkcs12" @@ -29,7 +29,7 @@ var ( // Use "" as the password argument if the PKCS#12 certificate is not password // protected. func FromP12File(filename string, password string) (tls.Certificate, error) { - p12bytes, err := ioutil.ReadFile(filename) + p12bytes, err := os.ReadFile(filename) if err != nil { return tls.Certificate{}, err } @@ -62,7 +62,7 @@ func FromP12Bytes(bytes []byte, password string) (tls.Certificate, error) { // Use "" as the password argument if the PEM certificate is not password // protected. func FromPemFile(filename string, password string) (tls.Certificate, error) { - bytes, err := ioutil.ReadFile(filename) + bytes, err := os.ReadFile(filename) if err != nil { return tls.Certificate{}, err } diff --git a/certificate/certificate_test.go b/certificate/certificate_test.go index b04eeac1..c52b3073 100644 --- a/certificate/certificate_test.go +++ b/certificate/certificate_test.go @@ -3,7 +3,7 @@ package certificate_test import ( "crypto/tls" "errors" - "io/ioutil" + "os" "testing" "github.com/sideshow/apns2/certificate" @@ -19,7 +19,7 @@ func TestValidCertificateFromP12File(t *testing.T) { } func TestValidCertificateFromP12Bytes(t *testing.T) { - bytes, _ := ioutil.ReadFile("_fixtures/certificate-valid.p12") + bytes, _ := os.ReadFile("_fixtures/certificate-valid.p12") cer, err := certificate.FromP12Bytes(bytes, "") assert.NoError(t, err) assert.NotEqual(t, tls.Certificate{}, cer) @@ -52,7 +52,7 @@ func TestValidCertificateFromPemFile(t *testing.T) { } func TestValidCertificateFromPemBytes(t *testing.T) { - bytes, _ := ioutil.ReadFile("_fixtures/certificate-valid.pem") + bytes, _ := os.ReadFile("_fixtures/certificate-valid.pem") cer, err := certificate.FromPemBytes(bytes, "") assert.NoError(t, err) assert.NotEqual(t, tls.Certificate{}, cer) @@ -65,7 +65,7 @@ func TestValidCertificateFromPemFileWithPKCS8PrivateKey(t *testing.T) { } func TestValidCertificateFromPemBytesWithPKCS8PrivateKey(t *testing.T) { - bytes, _ := ioutil.ReadFile("_fixtures/certificate-valid-pkcs8.pem") + bytes, _ := os.ReadFile("_fixtures/certificate-valid-pkcs8.pem") cer, err := certificate.FromPemBytes(bytes, "") assert.NoError(t, err) assert.NotEqual(t, tls.Certificate{}, cer) diff --git a/token/token.go b/token/token.go index 26fec563..bf26e1a9 100644 --- a/token/token.go +++ b/token/token.go @@ -5,7 +5,7 @@ import ( "crypto/x509" "encoding/pem" "errors" - "io/ioutil" + "os" "sync" "time" @@ -40,7 +40,7 @@ type Token struct { // AuthKeyFromFile loads a .p8 certificate from a local file and returns a // *ecdsa.PrivateKey. func AuthKeyFromFile(filename string) (*ecdsa.PrivateKey, error) { - bytes, err := ioutil.ReadFile(filename) + bytes, err := os.ReadFile(filename) if err != nil { return nil, err } diff --git a/token/token_test.go b/token/token_test.go index ef3c6fd1..87a45aab 100644 --- a/token/token_test.go +++ b/token/token_test.go @@ -5,7 +5,7 @@ import ( "crypto/elliptic" "crypto/rand" "errors" - "io/ioutil" + "os" "testing" "time" @@ -21,7 +21,7 @@ func TestValidTokenFromP8File(t *testing.T) { } func TestValidTokenFromP8Bytes(t *testing.T) { - bytes, _ := ioutil.ReadFile("_fixtures/authkey-valid.p8") + bytes, _ := os.ReadFile("_fixtures/authkey-valid.p8") _, err := token.AuthKeyFromBytes(bytes) assert.NoError(t, err) } From 83ca0a4a861ad44ad97eb8852d0f060720901b80 Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Mon, 12 Aug 2024 15:17:16 +1200 Subject: [PATCH 5/8] Feature/add missing fields (#231) * Add missing ExpiredToken field to response * Add ApnsUniqueID identifier for development push --- client.go | 2 +- client_test.go | 3 +++ response.go | 8 ++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/client.go b/client.go index cd98dd42..d36da1bc 100644 --- a/client.go +++ b/client.go @@ -192,6 +192,7 @@ func (c *Client) PushWithContext(ctx Context, n *Notification) (*Response, error r := &Response{} r.StatusCode = response.StatusCode r.ApnsID = response.Header.Get("apns-id") + r.ApnsUniqueID = response.Header.Get("apns-unique-id") decoder := json.NewDecoder(response.Body) if err := decoder.Decode(r); err != nil && err != io.EOF { @@ -234,5 +235,4 @@ func setHeaders(r *http.Request, n *Notification) { } else { r.Header.Set("apns-push-type", string(PushTypeAlert)) } - } diff --git a/client_test.go b/client_test.go index cca1349f..cfbdb6c6 100644 --- a/client_test.go +++ b/client_test.go @@ -370,9 +370,11 @@ func TestBadPayload(t *testing.T) { func Test200SuccessResponse(t *testing.T) { n := mockNotification() var apnsID = "02ABC856-EF8D-4E49-8F15-7B8A61D978D6" + var apnsUniqueID = "A6739D99-D92A-424B-A91E-BF012365BD4E" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("apns-id", apnsID) + w.Header().Set("apns-unique-id", apnsUniqueID) w.WriteHeader(http.StatusOK) })) defer server.Close() @@ -380,6 +382,7 @@ func Test200SuccessResponse(t *testing.T) { assert.NoError(t, err) assert.Equal(t, http.StatusOK, res.StatusCode) assert.Equal(t, apnsID, res.ApnsID) + assert.Equal(t, apnsUniqueID, res.ApnsUniqueID) assert.Equal(t, true, res.Sent()) } diff --git a/response.go b/response.go index 99d63456..55609d39 100644 --- a/response.go +++ b/response.go @@ -84,6 +84,9 @@ const ( // 405 The specified :method was not POST. ReasonMethodNotAllowed = "MethodNotAllowed" + // 410 The device token has expired. + ReasonExpiredToken = "ExpiredToken" + // 410 The device token is inactive for the specified topic. ReasonUnregistered = "Unregistered" @@ -132,6 +135,11 @@ type Response struct { // If the value of StatusCode is 410, this is the last time at which APNs // confirmed that the device token was no longer valid for the topic. Timestamp Time + + // An identifier that is only available in the Developement enviroment. Use + // this to query Delivery Log information for the corresponding notification + // in Push Notifications Console. + ApnsUniqueID string } // Sent returns whether or not the notification was successfully sent. From b1e286b448e7788d47cea53e2208ae1082aa9e52 Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Wed, 23 Oct 2024 21:17:15 +1300 Subject: [PATCH 6/8] [CH-5425] Fix apns-expiration request header logic (#9) (#237) Co-authored-by: Zijin Li --- client.go | 2 +- client_test.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/client.go b/client.go index d36da1bc..832b4210 100644 --- a/client.go +++ b/client.go @@ -227,7 +227,7 @@ func setHeaders(r *http.Request, n *Notification) { if n.Priority > 0 { r.Header.Set("apns-priority", strconv.Itoa(n.Priority)) } - if !n.Expiration.IsZero() { + if n.Expiration.After(time.Unix(0, 0)) { r.Header.Set("apns-expiration", strconv.FormatInt(n.Expiration.Unix(), 10)) } if n.PushType != "" { diff --git a/client_test.go b/client_test.go index cfbdb6c6..2f2045c1 100644 --- a/client_test.go +++ b/client_test.go @@ -245,6 +245,25 @@ func TestHeaders(t *testing.T) { assert.NoError(t, err) } +func TestExpirationHeader(t *testing.T) { + n := mockNotification() + n.ApnsID = "84DB694F-464F-49BD-960A-D6DB028335C9" + n.CollapseID = "game1.start.identifier" + n.Topic = "com.testapp" + n.Priority = 10 + n.Expiration = time.Unix(0, 0) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, n.ApnsID, r.Header.Get("apns-id")) + assert.Equal(t, n.CollapseID, r.Header.Get("apns-collapse-id")) + assert.Equal(t, "10", r.Header.Get("apns-priority")) + assert.Equal(t, n.Topic, r.Header.Get("apns-topic")) + assert.Equal(t, "", r.Header.Get("apns-expiration")) + })) + defer server.Close() + _, err := mockClient(server.URL).Push(n) + assert.NoError(t, err) +} + func TestPushTypeAlertHeader(t *testing.T) { n := mockNotification() n.PushType = apns.PushTypeAlert From 8c4111726360ef561225b64cc3cbf4e7e3402978 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 24 Oct 2024 11:42:40 +0800 Subject: [PATCH 7/8] Fix n.Expiration.IsZero and support pushtotalk push notification (#235) --- client_test.go | 11 +++++++++++ notification.go | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/client_test.go b/client_test.go index 2f2045c1..60fd0585 100644 --- a/client_test.go +++ b/client_test.go @@ -352,6 +352,17 @@ func TestPushTypeLiveActivityHeader(t *testing.T) { assert.NoError(t, err) } +func TestPushTypePushToTalkHeader(t *testing.T) { + n := mockNotification() + n.PushType = apns.PushTypePushToTalk + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "pushtotalk", 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 5cbc921f..54c8cafd 100644 --- a/notification.go +++ b/notification.go @@ -71,6 +71,12 @@ const ( // push type is recommended for iOS. It is not available on macOS, tvOS, // watchOS and iPadOS. PushTypeLiveActivity EPushType = "liveactivity" + + // PushTypePushToTalk is used for notifications that provide information about the + // push to talk. If you set this push type, the apns-topic header field + // must use your app’s bundle ID with.voip-ptt appended to the end. + // The pushtotalk push type isn’t available on watchOS, macOS, and tvOS. It’s recommended on iOS and iPadOS. + PushTypePushToTalk EPushType = "pushtotalk" ) const ( From 22443bc957912b3edcea972c4103a91e0f95931f Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Thu, 24 Oct 2024 16:43:01 +1300 Subject: [PATCH 8/8] Change ioutil to io (#232) --- client_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client_test.go b/client_test.go index 60fd0585..9c83d781 100644 --- a/client_test.go +++ b/client_test.go @@ -8,7 +8,7 @@ import ( "crypto/tls" "errors" "fmt" - "io/ioutil" + "io" "net" "net/http" "net/http/httptest" @@ -381,7 +381,7 @@ func TestAuthorizationHeader(t *testing.T) { func TestPayload(t *testing.T) { n := mockNotification() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, err := ioutil.ReadAll(r.Body) + body, err := io.ReadAll(r.Body) assert.NoError(t, err) assert.Equal(t, n.Payload, body) }))