diff --git a/README.md b/README.md
index 1b66c4a4..0e515314 100644
--- a/README.md
+++ b/README.md
@@ -110,6 +110,7 @@ Yes, please! Contributions of all kinds are very welcome! Feel free to check our
| [WeChat](https://www.wechat.com) | [service/wechat](service/wechat) | [silenceper/wechat](https://github.com/silenceper/wechat) | :heavy_check_mark: |
| [Webpush Notification](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) | [service/webpush](service/webpush) | [SherClockHolmes/webpush-go](https://github.com/SherClockHolmes/webpush-go/) | :heavy_check_mark: |
| [WhatsApp](https://www.whatsapp.com) | [service/whatsapp](service/whatsapp) | [Rhymen/go-whatsapp](https://github.com/Rhymen/go-whatsapp) | :x: |
+| [APNS2 Notification] | [service/apns2](service/apns2) | [sideshow/apns2](https://github.com/sideshow/apns2) | :heavy_check_mark: |
## Special Thanks
diff --git a/go.mod b/go.mod
index 5685947f..2689370c 100644
--- a/go.mod
+++ b/go.mod
@@ -53,6 +53,7 @@ require (
github.com/go-chi/chi/v5 v5.0.8 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.16.0 // indirect
+ github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect
@@ -61,6 +62,7 @@ require (
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/rs/zerolog v1.30.0 // indirect
+ github.com/sideshow/apns2 v0.23.0 // indirect
go.mau.fi/util v0.1.0 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
@@ -114,10 +116,10 @@ require (
github.com/tidwall/sjson v1.2.5 // indirect
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect
github.com/ttacon/libphonenumber v1.2.1 // indirect
- golang.org/x/crypto v0.13.0 // indirect
- golang.org/x/net v0.15.0 // indirect
+ golang.org/x/crypto v0.14.0 // indirect
+ golang.org/x/net v0.16.0 // indirect
golang.org/x/oauth2 v0.12.0 // indirect
- golang.org/x/sys v0.12.0 // indirect
+ golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.31.0 // indirect
diff --git a/go.sum b/go.sum
index 84f24cff..d268a494 100644
--- a/go.sum
+++ b/go.sum
@@ -11,6 +11,8 @@ github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20221121042443-a3fd332d56d9 h1:v
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20221121042443-a3fd332d56d9/go.mod h1:rjP7sIipbZcagro/6TCk6X0ZeFT2eyudH5+fve/cbBA=
github.com/SherClockHolmes/webpush-go v1.2.0 h1:sGv0/ZWCvb1HUH+izLqrb2i68HuqD/0Y+AmGQfyqKJA=
github.com/SherClockHolmes/webpush-go v1.2.0/go.mod h1:w6X47YApe/B9wUz2Wh8xukxlyupaxSSEbu6yKJcHN2w=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20201120081800-1786d5ef83d4/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.30.0 h1:uA3uhDbCxfO9+DI/DuGeAMr9qI+noVWwGPNTFuKID5M=
@@ -128,6 +130,9 @@ github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZg
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
+github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
+github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -263,6 +268,8 @@ github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekuei
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.13.0+incompatible h1:HZrzc06/QfBGesY9o3n1lvBrRONA+57rbDRKet7plos=
github.com/sendgrid/sendgrid-go v3.13.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
+github.com/sideshow/apns2 v0.23.0 h1:lpkikaZ995GIcKk6AFsYzHyezCrsrfEDvUWcWkEGErY=
+github.com/sideshow/apns2 v0.23.0/go.mod h1:7Fceu+sL0XscxrfLSkAoH6UtvKefq3Kq1n4W3ayQZqE=
github.com/silenceper/wechat/v2 v2.1.5 h1:eIlv61v2bAFBG9ZE75zuRC0ALHEZEUq8JlJ9tfKvatg=
github.com/silenceper/wechat/v2 v2.1.5/go.mod h1:7Iu3EhQYVtDUJAj+ZVRy8yom75ga7aDWv8RurLkVm0s=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
@@ -280,6 +287,7 @@ github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -319,6 +327,7 @@ go.mau.fi/util v0.1.0 h1:BwIFWIOEeO7lsiI2eWKFkWTfc5yQmoe+0FYyOFVyaoE=
go.mau.fi/util v0.1.0/go.mod h1:AxuJUMCxpzgJ5eV9JbPWKRH8aAJJidxetNdUj7qcb84=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+golang.org/x/crypto v0.0.0-20170512130425-ab89591268e0/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -328,6 +337,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
@@ -354,10 +365,13 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
+golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
@@ -397,6 +411,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -456,6 +471,7 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
diff --git a/service/apns2/README.md b/service/apns2/README.md
new file mode 100644
index 00000000..9be2b70f
--- /dev/null
+++ b/service/apns2/README.md
@@ -0,0 +1,27 @@
+# APNS2
+
+[APNS2](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns) sending notifications through the apns server directly to iphone devices
+
+## Usage
+
+```go
+// Create a apns2 service. `service.p12` or `service.pem` is generated when you install the application.
+apns2Service := apns2.New(P12File("/cert/service.p12",""),"")
+apns2Service = apns2.New(P12Bytes([]byte{},""),"")
+apns2Service = apns2.New(PemFile("/cert/service.pem",""),"")
+apns2Service = apns2.New(PemBytes([]byte{},""),"")
+
+// Add devices
+apns2Service.AddReceivers("","")
+
+// Tell our notifier to use the apns2 service.
+notify.UseServices(apns2Service)
+
+// Send a test message.
+_ = notify.Send(
+ context.Background(),
+ "Subject/Title",
+ "The actual message - Hello, you awesome gophers! :)",
+)
+```
+
diff --git a/service/apns2/apns2.go b/service/apns2/apns2.go
new file mode 100644
index 00000000..85f702bf
--- /dev/null
+++ b/service/apns2/apns2.go
@@ -0,0 +1,126 @@
+package apns2
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/pkg/errors"
+ apnsSvc "github.com/sideshow/apns2"
+ "github.com/sideshow/apns2/certificate"
+)
+
+// Compile-time check that fcm.Client satisfies fcmClient interface.
+var _ apns2Client = &apnsSvc.Client{}
+
+//go:generate mockery --name=apns2Client --output=. --case=underscore --inpackage
+type apns2Client interface {
+ Push(n *apnsSvc.Notification) (*apnsSvc.Response, error)
+}
+
+// hook to parse p12 bytes for credentials
+func P12Bytes(bytes []byte, password string) func() (apns2Client, error) {
+ return func() (apns2Client, error) {
+ cert, err := certificate.FromP12Bytes(bytes, password)
+ if err != nil {
+ return nil, errors.Wrapf(err, "invalid certificates %s %s", bytes, password)
+ }
+
+ client := apnsSvc.NewClient(cert).Production()
+ return client, nil
+ }
+}
+
+// hook to parse p12 file for credentials
+func P12File(filename, password string) func() (apns2Client, error) {
+ return func() (apns2Client, error) {
+ cert, err := certificate.FromP12File(filename, password)
+ if err != nil {
+ return nil, errors.Wrapf(err, "invalid certificates %s %s", filename, password)
+ }
+
+ client := apnsSvc.NewClient(cert).Production()
+ return client, nil
+ }
+}
+
+// hook to parse pem file for credentials
+func PemFile(filename, password string) func() (apns2Client, error) {
+ return func() (apns2Client, error) {
+ cert, err := certificate.FromPemFile(filename, password)
+ if err != nil {
+ return nil, errors.Wrapf(err, "invalid certificates %s %s", filename, password)
+ }
+
+ client := apnsSvc.NewClient(cert).Production()
+ return client, nil
+ }
+}
+
+// hook to parse pem bytes for credentials
+func PemBytes(bytes []byte, password string) func() (apns2Client, error) {
+ return func() (apns2Client, error) {
+ cert, err := certificate.FromPemBytes(bytes, password)
+ if err != nil {
+ return nil, errors.Wrapf(err, "invalid certificates %s %s", bytes, password)
+ }
+
+ client := apnsSvc.NewClient(cert).Production()
+ return client, nil
+ }
+}
+
+func buildNotification(token, topic, msg string) *apnsSvc.Notification {
+ notification := &apnsSvc.Notification{}
+ notification.DeviceToken = token
+ notification.Topic = topic
+ notification.Payload = []byte(fmt.Sprintf(`{"aps":{"alert":"%s"}}`, msg))
+
+ return notification
+}
+
+// Service encapsulates the APNS2 client along with internal state for storing device tokens.
+type Service struct {
+ client apns2Client
+ topic string
+ deviceTokens []string
+}
+
+// New returns a new instance of a APNS2 notification service
+func New(makeClient func() (apns2Client, error), topic string) (*Service, error) {
+ apnsClient, err := makeClient()
+ if err != nil {
+ return nil, err
+ }
+
+ client := &Service{
+ apnsClient,
+ topic,
+ make([]string, 0),
+ }
+ return client, nil
+}
+
+// AddReceivers takes APNS2 device tokens and appends them to the internal device tokens slice.
+// The Send method will send a given message to all those devices.
+func (s *Service) AddReceivers(deviceTokens ...string) {
+ s.deviceTokens = append(s.deviceTokens, deviceTokens...)
+}
+
+// Send takes a message subject and a message body and sends them to all previously set devices.
+func (s *Service) Send(ctx context.Context, subject, message string) error {
+ for _, deviceToken := range s.deviceTokens {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ default:
+ notification := buildNotification(deviceToken, s.topic, subject+" "+message)
+
+ _, err := s.client.Push(notification)
+ if err != nil {
+ return errors.Wrapf(err, "failed to send notification to %s", deviceToken)
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/service/apns2/apns2_test.go b/service/apns2/apns2_test.go
new file mode 100644
index 00000000..49ffbd35
--- /dev/null
+++ b/service/apns2/apns2_test.go
@@ -0,0 +1,58 @@
+package apns2
+
+import (
+ "context"
+ "testing"
+
+ apnsSvc "github.com/sideshow/apns2"
+ "github.com/stretchr/testify/require"
+)
+
+func TestAPNS2_AddReceivers(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ svc := &Service{
+ deviceTokens: []string{},
+ }
+ deviceTokens := []string{"Token1", "Token2", "Token3"}
+ svc.AddReceivers(deviceTokens...)
+
+ assert.Equal(svc.deviceTokens, deviceTokens)
+}
+
+func TestBuildNotification(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+ notification := buildNotification("", "", "test notification")
+ assert.IsType(new(apnsSvc.Notification), notification)
+
+ assert.Equal(notification.Topic, "")
+ assert.Equal(notification.DeviceToken, "")
+}
+
+func TestSend(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ client := &mockApns2Client{}
+ notification := buildNotification("", "", "subject message")
+ client.On("Push", notification).Return(&apnsSvc.Response{
+ StatusCode: 200,
+ Reason: "",
+ ApnsID: "",
+ Timestamp: apnsSvc.Time{},
+ }, nil)
+
+ svc := &Service{
+ client: client,
+ topic: "",
+ deviceTokens: []string{""},
+ }
+ err := svc.Send(context.Background(), "subject", "message")
+
+ assert.Nil(err)
+}
diff --git a/service/apns2/doc.go b/service/apns2/doc.go
new file mode 100644
index 00000000..8e5f5b97
--- /dev/null
+++ b/service/apns2/doc.go
@@ -0,0 +1,39 @@
+/*
+Package apns2 provides a service for sending notifications to ios.
+
+Usage:
+
+ package main
+
+ import (
+ "context"
+ "log"
+
+ "github.com/nikoksr/notify"
+ "github.com/nikoksr/notify/service/apns2"
+ )
+
+ func main() {
+
+ // Create a apns2 service. `service.p12` or `service.pem` is generated when you install the application.
+ apns2Service := apns2.New(P12File("/cert/service.p12",""),"")
+ apns2Service = apns2.New(P12Bytes([]byte{},""),"")
+ apns2Service = apns2.New(PemFile("/cert/service.pem",""),"")
+ apns2Service = apns2.New(PemBytes([]byte{},""),"")
+
+ // Add devices
+ apns2Service.AddReceivers("","")
+
+ // Tell our notifier to use the apns2 service.
+ notify.UseServices(apns2Service)
+
+ // Send a test message.
+ _ = notify.Send(
+ context.Background(),
+ "Subject/Title",
+ "The actual message - Hello, you awesome gophers! :)",
+ )
+
+ }
+*/
+package apns2
diff --git a/service/apns2/mock_apns2_client.go b/service/apns2/mock_apns2_client.go
new file mode 100644
index 00000000..61feb754
--- /dev/null
+++ b/service/apns2/mock_apns2_client.go
@@ -0,0 +1,53 @@
+// Code generated by mockery v2.35.1. DO NOT EDIT.
+
+package apns2
+
+import (
+ sideshowapns2 "github.com/sideshow/apns2"
+ mock "github.com/stretchr/testify/mock"
+)
+
+// mockApns2Client is an autogenerated mock type for the apns2Client type
+type mockApns2Client struct {
+ mock.Mock
+}
+
+// Push provides a mock function with given fields: n
+func (_m *mockApns2Client) Push(n *sideshowapns2.Notification) (*sideshowapns2.Response, error) {
+ ret := _m.Called(n)
+
+ var r0 *sideshowapns2.Response
+ var r1 error
+ if rf, ok := ret.Get(0).(func(*sideshowapns2.Notification) (*sideshowapns2.Response, error)); ok {
+ return rf(n)
+ }
+ if rf, ok := ret.Get(0).(func(*sideshowapns2.Notification) *sideshowapns2.Response); ok {
+ r0 = rf(n)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*sideshowapns2.Response)
+ }
+ }
+
+ if rf, ok := ret.Get(1).(func(*sideshowapns2.Notification) error); ok {
+ r1 = rf(n)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
+// newMockApns2Client creates a new instance of mockApns2Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func newMockApns2Client(t interface {
+ mock.TestingT
+ Cleanup(func())
+}) *mockApns2Client {
+ mock := &mockApns2Client{}
+ mock.Mock.Test(t)
+
+ t.Cleanup(func() { mock.AssertExpectations(t) })
+
+ return mock
+}