Skip to content

Commit bc3cef2

Browse files
marvelmflovilmart
authored andcommitted
Support local time for scheduled pushes (#4137)
* Handle local push time * PR feedback * Improve timezone detection with regex * Use indexOf instead of endsWith * Add documentation * Add end to end test for scheduled pushes in local time * Revert changes to npm-git script * clean up
1 parent 21f4411 commit bc3cef2

File tree

3 files changed

+151
-16
lines changed

3 files changed

+151
-16
lines changed

spec/PushController.spec.js

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,8 @@ describe('PushController', () => {
138138
'push_time': timeStr
139139
}
140140

141-
var time = PushController.getPushTime(body);
142-
expect(time).toEqual(new Date(timeStr));
141+
var { date } = PushController.getPushTime(body);
142+
expect(date).toEqual(new Date(timeStr));
143143
done();
144144
});
145145

@@ -150,8 +150,8 @@ describe('PushController', () => {
150150
'push_time': timeNumber
151151
}
152152

153-
var time = PushController.getPushTime(body).valueOf();
154-
expect(time).toEqual(timeNumber * 1000);
153+
var { date } = PushController.getPushTime(body);
154+
expect(date.valueOf()).toEqual(timeNumber * 1000);
155155
done();
156156
});
157157

@@ -640,16 +640,36 @@ describe('PushController', () => {
640640
expect(PushController.getPushTime()).toBe(undefined);
641641
expect(PushController.getPushTime({
642642
'push_time': 1000
643-
})).toEqual(new Date(1000 * 1000));
643+
}).date).toEqual(new Date(1000 * 1000));
644644
expect(PushController.getPushTime({
645645
'push_time': '2017-01-01'
646-
})).toEqual(new Date('2017-01-01'));
646+
}).date).toEqual(new Date('2017-01-01'));
647+
647648
expect(() => {PushController.getPushTime({
648649
'push_time': 'gibberish-time'
649650
})}).toThrow();
650651
expect(() => {PushController.getPushTime({
651652
'push_time': Number.NaN
652653
})}).toThrow();
654+
655+
expect(PushController.getPushTime({
656+
push_time: '2017-09-06T13:42:48.369Z'
657+
})).toEqual({
658+
date: new Date('2017-09-06T13:42:48.369Z'),
659+
isLocalTime: false,
660+
});
661+
expect(PushController.getPushTime({
662+
push_time: '2007-04-05T12:30-02:00',
663+
})).toEqual({
664+
date: new Date('2007-04-05T12:30-02:00'),
665+
isLocalTime: false,
666+
});
667+
expect(PushController.getPushTime({
668+
push_time: '2007-04-05T12:30',
669+
})).toEqual({
670+
date: new Date('2007-04-05T12:30'),
671+
isLocalTime: true,
672+
});
653673
});
654674

655675
it('should not schedule push when not configured', (done) => {
@@ -979,4 +999,86 @@ describe('PushController', () => {
979999
done();
9801000
}).catch(done.fail);
9811001
});
1002+
1003+
describe('pushTimeHasTimezoneComponent', () => {
1004+
it('should be accurate', () => {
1005+
expect(PushController.pushTimeHasTimezoneComponent('2017-09-06T17:14:01.048Z'))
1006+
.toBe(true, 'UTC time');
1007+
expect(PushController.pushTimeHasTimezoneComponent('2007-04-05T12:30-02:00'))
1008+
.toBe(true, 'Timezone offset');
1009+
expect(PushController.pushTimeHasTimezoneComponent('2007-04-05T12:30:00.000Z-02:00'))
1010+
.toBe(true, 'Seconds + Milliseconds + Timezone offset');
1011+
1012+
expect(PushController.pushTimeHasTimezoneComponent('2017-09-06T17:14:01.048'))
1013+
.toBe(false, 'No timezone');
1014+
expect(PushController.pushTimeHasTimezoneComponent('2017-09-06'))
1015+
.toBe(false, 'YY-MM-DD');
1016+
});
1017+
});
1018+
1019+
describe('formatPushTime', () => {
1020+
it('should format as ISO string', () => {
1021+
expect(PushController.formatPushTime({
1022+
date: new Date('2017-09-06T17:14:01.048Z'),
1023+
isLocalTime: false,
1024+
})).toBe('2017-09-06T17:14:01.048Z', 'UTC time');
1025+
expect(PushController.formatPushTime({
1026+
date: new Date('2007-04-05T12:30-02:00'),
1027+
isLocalTime: false
1028+
})).toBe('2007-04-05T14:30:00.000Z', 'Timezone offset');
1029+
1030+
expect(PushController.formatPushTime({
1031+
date: new Date('2017-09-06T17:14:01.048'),
1032+
isLocalTime: true,
1033+
})).toBe('2017-09-06T17:14:01.048', 'No timezone');
1034+
expect(PushController.formatPushTime({
1035+
date: new Date('2017-09-06'),
1036+
isLocalTime: true
1037+
})).toBe('2017-09-06T00:00:00.000', 'YY-MM-DD');
1038+
});
1039+
});
1040+
1041+
describe('Scheduling pushes in local time', () => {
1042+
it('should preserve the push time', (done) => {
1043+
const auth = {isMaster: true};
1044+
const pushAdapter = {
1045+
send(body, installations) {
1046+
return successfulTransmissions(body, installations);
1047+
},
1048+
getValidPushTypes() {
1049+
return ["ios"];
1050+
}
1051+
};
1052+
1053+
const pushTime = '2017-09-06T17:14:01.048';
1054+
1055+
reconfigureServer({
1056+
push: {adapter: pushAdapter},
1057+
scheduledPush: true
1058+
})
1059+
.then(() => {
1060+
const config = new Config(Parse.applicationId);
1061+
return new Promise((resolve, reject) => {
1062+
const pushController = new PushController();
1063+
pushController.sendPush({
1064+
data: {
1065+
alert: "Hello World!",
1066+
badge: "Increment",
1067+
},
1068+
push_time: pushTime
1069+
}, {}, config, auth, resolve)
1070+
.catch(reject);
1071+
})
1072+
})
1073+
.then((pushStatusId) => {
1074+
const q = new Parse.Query('_PushStatus');
1075+
return q.get(pushStatusId, {useMasterKey: true});
1076+
})
1077+
.then((pushStatus) => {
1078+
expect(pushStatus.get('status')).toBe('scheduled');
1079+
expect(pushStatus.get('pushTime')).toBe('2017-09-06T17:14:01.048');
1080+
})
1081+
.then(done, done.fail);
1082+
});
1083+
});
9821084
});

src/Controllers/PushController.js

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ export class PushController {
1414
}
1515
// Replace the expiration_time and push_time with a valid Unix epoch milliseconds time
1616
body.expiration_time = PushController.getExpirationTime(body);
17-
const push_time = PushController.getPushTime(body);
18-
if (typeof push_time !== 'undefined') {
19-
body['push_time'] = push_time;
17+
const pushTime = PushController.getPushTime(body);
18+
if (pushTime && pushTime.date !== 'undefined') {
19+
body['push_time'] = PushController.formatPushTime(pushTime);
2020
}
21+
2122
// TODO: If the req can pass the checking, we return immediately instead of waiting
2223
// pushes to be sent. We probably change this behaviour in the future.
2324
let badgeUpdate = () => {
@@ -104,21 +105,53 @@ export class PushController {
104105
return;
105106
}
106107
var pushTimeParam = body['push_time'];
107-
var pushTime;
108+
var date;
109+
var isLocalTime = true;
110+
108111
if (typeof pushTimeParam === 'number') {
109-
pushTime = new Date(pushTimeParam * 1000);
112+
date = new Date(pushTimeParam * 1000);
110113
} else if (typeof pushTimeParam === 'string') {
111-
pushTime = new Date(pushTimeParam);
114+
isLocalTime = !PushController.pushTimeHasTimezoneComponent(pushTimeParam);
115+
date = new Date(pushTimeParam);
112116
} else {
113117
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
114118
body['push_time'] + ' is not valid time.');
115119
}
116120
// Check pushTime is valid or not, if it is not valid, pushTime is NaN
117-
if (!isFinite(pushTime)) {
121+
if (!isFinite(date)) {
118122
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
119123
body['push_time'] + ' is not valid time.');
120124
}
121-
return pushTime;
125+
126+
return {
127+
date,
128+
isLocalTime,
129+
};
130+
}
131+
132+
/**
133+
* Checks if a ISO8601 formatted date contains a timezone component
134+
* @param pushTimeParam {string}
135+
* @returns {boolean}
136+
*/
137+
static pushTimeHasTimezoneComponent(pushTimeParam: string): boolean {
138+
const offsetPattern = /(.+)([+-])\d\d:\d\d$/;
139+
return pushTimeParam.indexOf('Z') === pushTimeParam.length - 1 // 2007-04-05T12:30Z
140+
|| offsetPattern.test(pushTimeParam); // 2007-04-05T12:30.000+02:00, 2007-04-05T12:30.000-02:00
141+
}
142+
143+
/**
144+
* Converts a date to ISO format in UTC time and strips the timezone if `isLocalTime` is true
145+
* @param date {Date}
146+
* @param isLocalTime {boolean}
147+
* @returns {string}
148+
*/
149+
static formatPushTime({ date, isLocalTime }: { date: Date, isLocalTime: boolean }) {
150+
if (isLocalTime) { // Strip 'Z'
151+
const isoString = date.toISOString();
152+
return isoString.substring(0, isoString.indexOf('Z'));
153+
}
154+
return date.toISOString();
122155
}
123156
}
124157

src/StatusHandler.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export function pushStatusHandler(config, objectId = newObjectId(config.objectId
110110
const handler = statusHandler(PUSH_STATUS_COLLECTION, database);
111111
const setInitial = function(body = {}, where, options = {source: 'rest'}) {
112112
const now = new Date();
113-
let pushTime = new Date();
113+
let pushTime = now.toISOString();
114114
let status = 'pending';
115115
if (body.hasOwnProperty('push_time')) {
116116
if (config.hasPushScheduledSupport) {
@@ -135,7 +135,7 @@ export function pushStatusHandler(config, objectId = newObjectId(config.objectId
135135
const object = {
136136
objectId,
137137
createdAt: now,
138-
pushTime: pushTime.toISOString(),
138+
pushTime,
139139
query: JSON.stringify(where),
140140
payload: payloadString,
141141
source: options.source,

0 commit comments

Comments
 (0)