feat(ios): Support iOS 12 Critical Alerts (#367)
fix https://github.com/appleboy/gorush/issues/366
This commit is contained in:
		
							parent
							
								
									880752d88c
								
							
						
					
					
						commit
						0c89fd1d81
					
				|  | @ -50,16 +50,16 @@ type RequestPush struct { | ||||||
| // PushNotification is single notification request
 | // PushNotification is single notification request
 | ||||||
| type PushNotification struct { | type PushNotification struct { | ||||||
| 	// Common
 | 	// Common
 | ||||||
| 	Tokens           []string `json:"tokens" binding:"required"` | 	Tokens           []string    `json:"tokens" binding:"required"` | ||||||
| 	Platform         int      `json:"platform" binding:"required"` | 	Platform         int         `json:"platform" binding:"required"` | ||||||
| 	Message          string   `json:"message,omitempty"` | 	Message          string      `json:"message,omitempty"` | ||||||
| 	Title            string   `json:"title,omitempty"` | 	Title            string      `json:"title,omitempty"` | ||||||
| 	Priority         string   `json:"priority,omitempty"` | 	Priority         string      `json:"priority,omitempty"` | ||||||
| 	ContentAvailable bool     `json:"content_available,omitempty"` | 	ContentAvailable bool        `json:"content_available,omitempty"` | ||||||
| 	MutableContent   bool     `json:"mutable_content,omitempty"` | 	MutableContent   bool        `json:"mutable_content,omitempty"` | ||||||
| 	Sound            string   `json:"sound,omitempty"` | 	Sound            interface{} `json:"sound,omitempty"` | ||||||
| 	Data             D        `json:"data,omitempty"` | 	Data             D           `json:"data,omitempty"` | ||||||
| 	Retry            int      `json:"retry,omitempty"` | 	Retry            int         `json:"retry,omitempty"` | ||||||
| 	wg               *sync.WaitGroup | 	wg               *sync.WaitGroup | ||||||
| 	log              *[]LogPushEntry | 	log              *[]LogPushEntry | ||||||
| 
 | 
 | ||||||
|  | @ -86,6 +86,8 @@ type PushNotification struct { | ||||||
| 	Alert       Alert    `json:"alert,omitempty"` | 	Alert       Alert    `json:"alert,omitempty"` | ||||||
| 	Production  bool     `json:"production,omitempty"` | 	Production  bool     `json:"production,omitempty"` | ||||||
| 	Development bool     `json:"development,omitempty"` | 	Development bool     `json:"development,omitempty"` | ||||||
|  | 	SoundName   string   `json:"name,omitempty"` | ||||||
|  | 	SoundVolume float32  `json:"volume,omitempty"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // WaitDone decrements the WaitGroup counter.
 | // WaitDone decrements the WaitGroup counter.
 | ||||||
|  |  | ||||||
|  | @ -14,6 +14,13 @@ import ( | ||||||
| 	"github.com/sideshow/apns2/token" | 	"github.com/sideshow/apns2/token" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // Sound sets the aps sound on the payload.
 | ||||||
|  | type Sound struct { | ||||||
|  | 	Critical int     `json:"critical,omitempty"` | ||||||
|  | 	Name     string  `json:"name,omitempty"` | ||||||
|  | 	Volume   float32 `json:"volume,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // InitAPNSClient use for initialize APNs Client.
 | // InitAPNSClient use for initialize APNs Client.
 | ||||||
| func InitAPNSClient() error { | func InitAPNSClient() error { | ||||||
| 	if PushConf.Ios.Enabled { | 	if PushConf.Ios.Enabled { | ||||||
|  | @ -183,8 +190,16 @@ func GetIOSNotification(req PushNotification) *apns2.Notification { | ||||||
| 		payload.MutableContent() | 		payload.MutableContent() | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if len(req.Sound) > 0 { | 	if _, ok := req.Sound.(Sound); ok { | ||||||
| 		payload.Sound(req.Sound) | 		payload.Sound(&req.Sound) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(req.SoundName) > 0 { | ||||||
|  | 		payload.SoundName(req.SoundName) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if req.SoundVolume > 0 { | ||||||
|  | 		payload.SoundVolume(req.SoundVolume) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if req.ContentAvailable { | 	if req.ContentAvailable { | ||||||
|  |  | ||||||
|  | @ -56,13 +56,17 @@ func TestIOSNotificationStructure(t *testing.T) { | ||||||
| 	expectBadge := 0 | 	expectBadge := 0 | ||||||
| 	message := "Welcome notification Server" | 	message := "Welcome notification Server" | ||||||
| 	req := PushNotification{ | 	req := PushNotification{ | ||||||
| 		ApnsID:           test, | 		ApnsID:     test, | ||||||
| 		Topic:            test, | 		Topic:      test, | ||||||
| 		Expiration:       time.Now().Unix(), | 		Expiration: time.Now().Unix(), | ||||||
| 		Priority:         "normal", | 		Priority:   "normal", | ||||||
| 		Message:          message, | 		Message:    message, | ||||||
| 		Badge:            &expectBadge, | 		Badge:      &expectBadge, | ||||||
| 		Sound:            test, | 		Sound: Sound{ | ||||||
|  | 			Critical: 1, | ||||||
|  | 			Name:     test, | ||||||
|  | 			Volume:   1.0, | ||||||
|  | 		}, | ||||||
| 		ContentAvailable: true, | 		ContentAvailable: true, | ||||||
| 		Data: D{ | 		Data: D{ | ||||||
| 			"key1": "test", | 			"key1": "test", | ||||||
|  | @ -78,13 +82,14 @@ func TestIOSNotificationStructure(t *testing.T) { | ||||||
| 	data := []byte(string(dump)) | 	data := []byte(string(dump)) | ||||||
| 
 | 
 | ||||||
| 	if err := json.Unmarshal(data, &dat); err != nil { | 	if err := json.Unmarshal(data, &dat); err != nil { | ||||||
| 		log.Println(err) |  | ||||||
| 		panic(err) | 		panic(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	alert, _ := jsonparser.GetString(data, "aps", "alert") | 	alert, _ := jsonparser.GetString(data, "aps", "alert") | ||||||
| 	badge, _ := jsonparser.GetInt(data, "aps", "badge") | 	badge, _ := jsonparser.GetInt(data, "aps", "badge") | ||||||
| 	sound, _ := jsonparser.GetString(data, "aps", "sound") | 	soundName, _ := jsonparser.GetString(data, "aps", "sound", "name") | ||||||
|  | 	soundCritical, _ := jsonparser.GetInt(data, "aps", "sound", "critical") | ||||||
|  | 	soundVolume, _ := jsonparser.GetFloat(data, "aps", "sound", "volume") | ||||||
| 	contentAvailable, _ := jsonparser.GetInt(data, "aps", "content-available") | 	contentAvailable, _ := jsonparser.GetInt(data, "aps", "content-available") | ||||||
| 	category, _ := jsonparser.GetString(data, "aps", "category") | 	category, _ := jsonparser.GetString(data, "aps", "category") | ||||||
| 	key1 := dat["key1"].(interface{}) | 	key1 := dat["key1"].(interface{}) | ||||||
|  | @ -99,7 +104,9 @@ func TestIOSNotificationStructure(t *testing.T) { | ||||||
| 	assert.Equal(t, message, alert) | 	assert.Equal(t, message, alert) | ||||||
| 	assert.Equal(t, expectBadge, int(badge)) | 	assert.Equal(t, expectBadge, int(badge)) | ||||||
| 	assert.Equal(t, expectBadge, *req.Badge) | 	assert.Equal(t, expectBadge, *req.Badge) | ||||||
| 	assert.Equal(t, test, sound) | 	assert.Equal(t, test, soundName) | ||||||
|  | 	assert.Equal(t, 1.0, soundVolume) | ||||||
|  | 	assert.Equal(t, int64(1), soundCritical) | ||||||
| 	assert.Equal(t, 1, int(contentAvailable)) | 	assert.Equal(t, 1, int(contentAvailable)) | ||||||
| 	assert.Equal(t, "test", key1) | 	assert.Equal(t, "test", key1) | ||||||
| 	assert.Equal(t, 2, int(key2.(float64))) | 	assert.Equal(t, 2, int(key2.(float64))) | ||||||
|  | @ -108,6 +115,48 @@ func TestIOSNotificationStructure(t *testing.T) { | ||||||
| 	assert.Contains(t, urlArgs, "b") | 	assert.Contains(t, urlArgs, "b") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func TestIOSSoundAndVolume(t *testing.T) { | ||||||
|  | 	var dat map[string]interface{} | ||||||
|  | 
 | ||||||
|  | 	test := "test" | ||||||
|  | 	message := "Welcome notification Server" | ||||||
|  | 	req := PushNotification{ | ||||||
|  | 		ApnsID:   test, | ||||||
|  | 		Topic:    test, | ||||||
|  | 		Priority: "normal", | ||||||
|  | 		Message:  message, | ||||||
|  | 		Sound: Sound{ | ||||||
|  | 			Critical: 2, | ||||||
|  | 			Name:     test, | ||||||
|  | 			Volume:   1.0, | ||||||
|  | 		}, | ||||||
|  | 		SoundName:   "foo", | ||||||
|  | 		SoundVolume: 2.0, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	notification := GetIOSNotification(req) | ||||||
|  | 
 | ||||||
|  | 	dump, _ := json.Marshal(notification.Payload) | ||||||
|  | 	data := []byte(string(dump)) | ||||||
|  | 
 | ||||||
|  | 	if err := json.Unmarshal(data, &dat); err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	alert, _ := jsonparser.GetString(data, "aps", "alert") | ||||||
|  | 	soundName, _ := jsonparser.GetString(data, "aps", "sound", "name") | ||||||
|  | 	soundCritical, _ := jsonparser.GetInt(data, "aps", "sound", "critical") | ||||||
|  | 	soundVolume, _ := jsonparser.GetFloat(data, "aps", "sound", "volume") | ||||||
|  | 
 | ||||||
|  | 	assert.Equal(t, test, notification.ApnsID) | ||||||
|  | 	assert.Equal(t, test, notification.Topic) | ||||||
|  | 	assert.Equal(t, ApnsPriorityLow, notification.Priority) | ||||||
|  | 	assert.Equal(t, message, alert) | ||||||
|  | 	assert.Equal(t, "foo", soundName) | ||||||
|  | 	assert.Equal(t, 2.0, soundVolume) | ||||||
|  | 	assert.Equal(t, int64(1), soundCritical) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Silent Notification which payload’s aps dictionary must not contain the alert, sound, or badge keys.
 | // Silent Notification which payload’s aps dictionary must not contain the alert, sound, or badge keys.
 | ||||||
| // ref: https://goo.gl/m9xyqG
 | // ref: https://goo.gl/m9xyqG
 | ||||||
| func TestSendZeroValueForBadgeKey(t *testing.T) { | func TestSendZeroValueForBadgeKey(t *testing.T) { | ||||||
|  |  | ||||||
|  | @ -70,8 +70,8 @@ func GetAndroidNotification(req PushNotification) *fcm.Message { | ||||||
| 		notification.Notification.Title = req.Title | 		notification.Notification.Title = req.Title | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if len(req.Sound) > 0 { | 	if v, ok := req.Sound.(string); ok && len(v) > 0 { | ||||||
| 		notification.Notification.Sound = req.Sound | 		notification.Notification.Sound = v | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return notification | 	return notification | ||||||
|  |  | ||||||
|  | @ -1,7 +1,5 @@ | ||||||
| # APNS/2 | # APNS/2 | ||||||
| 
 | 
 | ||||||
| NOTE: This is an experimental branch for the purpose of testing the new token based authentication |  | ||||||
| 
 |  | ||||||
| APNS/2 is a go package designed for simple, flexible and fast Apple Push Notifications on iOS, OSX and Safari using the new HTTP/2 Push provider API. | APNS/2 is a go package designed for simple, flexible and fast Apple Push Notifications on iOS, OSX and Safari using the new HTTP/2 Push provider API. | ||||||
| 
 | 
 | ||||||
| [](https://travis-ci.org/sideshow/apns2)  [](https://coveralls.io/github/sideshow/apns2?branch=master)  [](https://godoc.org/github.com/sideshow/apns2) | [](https://travis-ci.org/sideshow/apns2)  [](https://coveralls.io/github/sideshow/apns2?branch=master)  [](https://godoc.org/github.com/sideshow/apns2) | ||||||
|  | @ -123,7 +121,7 @@ You can use raw bytes for the `notification.Payload` as above, or you can use th | ||||||
| ```go | ```go | ||||||
| // {"aps":{"alert":"hello","badge":1},"key":"val"} | // {"aps":{"alert":"hello","badge":1},"key":"val"} | ||||||
| 
 | 
 | ||||||
| payload := apns2.NewPayload().Alert("hello").Badge(1).Custom("key", "val") | payload := payload.NewPayload().Alert("hello").Badge(1).Custom("key", "val") | ||||||
| 
 | 
 | ||||||
| notification.Payload = payload | notification.Payload = payload | ||||||
| client.Push(notification) | client.Push(notification) | ||||||
|  | @ -156,7 +154,7 @@ if res.Sent() { | ||||||
| 
 | 
 | ||||||
| ## Context & Timeouts | ## Context & Timeouts | ||||||
| 
 | 
 | ||||||
| For better control over request cancelations and timeouts APNS/2 supports | For better control over request cancellations and timeouts APNS/2 supports | ||||||
| contexts. Using a context can be helpful if you want to cancel all pushes when | contexts. Using a context can be helpful if you want to cancel all pushes when | ||||||
| the parent process is cancelled, or need finer grained control over individual | the parent process is cancelled, or need finer grained control over individual | ||||||
| push timeouts. See the [Google post](https://blog.golang.org/context) for more | push timeouts. See the [Google post](https://blog.golang.org/context) for more | ||||||
|  |  | ||||||
|  | @ -133,13 +133,13 @@ func (c *Client) Production() *Client { | ||||||
| // indicating whether the notification was accepted or rejected by the APNs
 | // indicating whether the notification was accepted or rejected by the APNs
 | ||||||
| // gateway, or an error if something goes wrong.
 | // gateway, or an error if something goes wrong.
 | ||||||
| //
 | //
 | ||||||
| // Use PushWithContext if you need better cancelation and timeout control.
 | // Use PushWithContext if you need better cancellation and timeout control.
 | ||||||
| func (c *Client) Push(n *Notification) (*Response, error) { | func (c *Client) Push(n *Notification) (*Response, error) { | ||||||
| 	return c.PushWithContext(nil, n) | 	return c.PushWithContext(nil, n) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // PushWithContext sends a Notification to the APNs gateway. Context carries a
 | // PushWithContext sends a Notification to the APNs gateway. Context carries a
 | ||||||
| // deadline and a cancelation signal and allows you to close long running
 | // deadline and a cancellation signal and allows you to close long running
 | ||||||
| // requests when the context timeout is exceeded. Context can be nil, for
 | // requests when the context timeout is exceeded. Context can be nil, for
 | ||||||
| // backwards compatibility.
 | // backwards compatibility.
 | ||||||
| //
 | //
 | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ import ( | ||||||
| 	"golang.org/x/net/context/ctxhttp" | 	"golang.org/x/net/context/ctxhttp" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // A Context carries a deadline, a cancelation signal, and other values across
 | // A Context carries a deadline, a cancellation signal, and other values across
 | ||||||
| // API boundaries.
 | // API boundaries.
 | ||||||
| //
 | //
 | ||||||
| // Context's methods may be called by multiple goroutines simultaneously.
 | // Context's methods may be called by multiple goroutines simultaneously.
 | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // A Context carries a deadline, a cancelation signal, and other values across
 | // A Context carries a deadline, a cancellation signal, and other values across
 | ||||||
| // API boundaries.
 | // API boundaries.
 | ||||||
| //
 | //
 | ||||||
| // Context's methods may be called by multiple goroutines simultaneously.
 | // Context's methods may be called by multiple goroutines simultaneously.
 | ||||||
|  |  | ||||||
|  | @ -16,7 +16,7 @@ type aps struct { | ||||||
| 	Category         string      `json:"category,omitempty"` | 	Category         string      `json:"category,omitempty"` | ||||||
| 	ContentAvailable int         `json:"content-available,omitempty"` | 	ContentAvailable int         `json:"content-available,omitempty"` | ||||||
| 	MutableContent   int         `json:"mutable-content,omitempty"` | 	MutableContent   int         `json:"mutable-content,omitempty"` | ||||||
| 	Sound            string      `json:"sound,omitempty"` | 	Sound            interface{} `json:"sound,omitempty"` | ||||||
| 	ThreadID         string      `json:"thread-id,omitempty"` | 	ThreadID         string      `json:"thread-id,omitempty"` | ||||||
| 	URLArgs          []string    `json:"url-args,omitempty"` | 	URLArgs          []string    `json:"url-args,omitempty"` | ||||||
| } | } | ||||||
|  | @ -34,6 +34,12 @@ type alert struct { | ||||||
| 	TitleLocKey  string   `json:"title-loc-key,omitempty"` | 	TitleLocKey  string   `json:"title-loc-key,omitempty"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | type sound struct { | ||||||
|  | 	Critical int     `json:"critical,omitempty"` | ||||||
|  | 	Name     string  `json:"name,omitempty"` | ||||||
|  | 	Volume   float32 `json:"volume,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // NewPayload returns a new Payload struct
 | // NewPayload returns a new Payload struct
 | ||||||
| func NewPayload() *Payload { | func NewPayload() *Payload { | ||||||
| 	return &Payload{ | 	return &Payload{ | ||||||
|  | @ -84,7 +90,7 @@ func (p *Payload) UnsetBadge() *Payload { | ||||||
| // This will play a sound from the app bundle, or the default sound otherwise.
 | // This will play a sound from the app bundle, or the default sound otherwise.
 | ||||||
| //
 | //
 | ||||||
| //	{"aps":{"sound":sound}}
 | //	{"aps":{"sound":sound}}
 | ||||||
| func (p *Payload) Sound(sound string) *Payload { | func (p *Payload) Sound(sound interface{}) *Payload { | ||||||
| 	p.aps().Sound = sound | 	p.aps().Sound = sound | ||||||
| 	return p | 	return p | ||||||
| } | } | ||||||
|  | @ -274,6 +280,26 @@ func (p *Payload) URLArgs(urlArgs []string) *Payload { | ||||||
| 	return p | 	return p | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // SoundName sets the name value on the aps sound dictionary.
 | ||||||
|  | // This function makes the notification a critical alert, which should be pre-approved by Apple.
 | ||||||
|  | // See: https://developer.apple.com/contact/request/notifications-critical-alerts-entitlement/
 | ||||||
|  | //
 | ||||||
|  | // {"aps":{"sound":{"critical":1,"name":name,"volume":1.0}}}
 | ||||||
|  | func (p *Payload) SoundName(name string) *Payload { | ||||||
|  | 	p.aps().sound().Name = name | ||||||
|  | 	return p | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SoundVolume sets the volume value on the aps sound dictionary.
 | ||||||
|  | // This function makes the notification a critical alert, which should be pre-approved by Apple.
 | ||||||
|  | // See: https://developer.apple.com/contact/request/notifications-critical-alerts-entitlement/
 | ||||||
|  | //
 | ||||||
|  | // {"aps":{"sound":{"critical":1,"name":"default","volume":volume}}}
 | ||||||
|  | func (p *Payload) SoundVolume(volume float32) *Payload { | ||||||
|  | 	p.aps().sound().Volume = volume | ||||||
|  | 	return p | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // MarshalJSON returns the JSON encoded version of the Payload
 | // MarshalJSON returns the JSON encoded version of the Payload
 | ||||||
| func (p *Payload) MarshalJSON() ([]byte, error) { | func (p *Payload) MarshalJSON() ([]byte, error) { | ||||||
| 	return json.Marshal(p.content) | 	return json.Marshal(p.content) | ||||||
|  | @ -289,3 +315,10 @@ func (a *aps) alert() *alert { | ||||||
| 	} | 	} | ||||||
| 	return a.Alert.(*alert) | 	return a.Alert.(*alert) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (a *aps) sound() *sound { | ||||||
|  | 	if _, ok := a.Sound.(*sound); !ok { | ||||||
|  | 		a.Sound = &sound{Critical: 1, Name: "default", Volume: 1.0} | ||||||
|  | 	} | ||||||
|  | 	return a.Sound.(*sound) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -110,7 +110,7 @@ const ( | ||||||
| // surrounding the rejection.
 | // surrounding the rejection.
 | ||||||
| type Response struct { | type Response struct { | ||||||
| 
 | 
 | ||||||
| 	// The HTTP status code retuened by APNs.
 | 	// The HTTP status code returned by APNs.
 | ||||||
| 	// A 200 value indicates that the notification was successfully sent.
 | 	// A 200 value indicates that the notification was successfully sent.
 | ||||||
| 	// For a list of other possible status codes, see table 6-4 in the Apple Local
 | 	// For a list of other possible status codes, see table 6-4 in the Apple Local
 | ||||||
| 	// and Remote Notification Programming Guide.
 | 	// and Remote Notification Programming Guide.
 | ||||||
|  |  | ||||||
|  | @ -441,12 +441,12 @@ | ||||||
| 			"revisionTime": "2017-02-16T22:32:56Z" | 			"revisionTime": "2017-02-16T22:32:56Z" | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"checksumSHA1": "guPIkygGyVn+VPS1hh63f+4IJYg=", | 			"checksumSHA1": "DL5vX9aOefzIAxMDdaQBtgRdR0Y=", | ||||||
| 			"path": "github.com/sideshow/apns2", | 			"path": "github.com/sideshow/apns2", | ||||||
| 			"revision": "a3ce9c6f95f63dab4ead29da86534dd7af95271a", | 			"revision": "3c5d4af1700ed9111ecb16a9a99a92a00c8b2f27", | ||||||
| 			"revisionTime": "2017-09-26T09:37:56Z", | 			"revisionTime": "2018-08-27T05:51:07Z", | ||||||
| 			"version": "v0.13", | 			"version": "master", | ||||||
| 			"versionExact": "v0.13" | 			"versionExact": "master" | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"checksumSHA1": "EHOwwdWPJGt1vNVeJxyRDRHBhl8=", | 			"checksumSHA1": "EHOwwdWPJGt1vNVeJxyRDRHBhl8=", | ||||||
|  | @ -455,10 +455,10 @@ | ||||||
| 			"revisionTime": "2018-04-13T21:53:35Z" | 			"revisionTime": "2018-04-13T21:53:35Z" | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"checksumSHA1": "9Lxz0w+q8pqp/Do/kNa8hLoQeY0=", | 			"checksumSHA1": "36bPwB1aiQCxfmdwfgYmI89I9nI=", | ||||||
| 			"path": "github.com/sideshow/apns2/payload", | 			"path": "github.com/sideshow/apns2/payload", | ||||||
| 			"revision": "c6554aff77e6e5580dec977c8c33cc238f329ab0", | 			"revision": "3c5d4af1700ed9111ecb16a9a99a92a00c8b2f27", | ||||||
| 			"revisionTime": "2018-04-13T21:53:35Z" | 			"revisionTime": "2018-08-27T05:51:07Z" | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"checksumSHA1": "nkQ/1JoIY4jh8XlI8LClfFVux9U=", | 			"checksumSHA1": "nkQ/1JoIY4jh8XlI8LClfFVux9U=", | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue