Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Apple Wallet code (#76) #91

Merged
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
df21a4d
refactor(apple): converting to/from milliseconds (#76)
aahna-ashina Oct 13, 2022
293fc43
refactor(apple): add dateutils class (#76)
aahna-ashina Oct 16, 2022
f7d00b5
refactor(apple): add dateutils class (#76)
aahna-ashina Oct 16, 2022
e55ff6b
Merge branch 'main' into dw-724/refactor-apple-wallet-code
aahna-ashina Oct 17, 2022
ecf3d0c
refactor(apple):reduce the number of branches of code in [passTypeIde…
aahna-ashina Oct 17, 2022
36f5e8d
refactor(apple): use await instead of asynchronous (#76)
aahna-ashina Oct 19, 2022
68711f2
Merge branch 'main' into dw-724/refactor-apple-wallet-code
aahna-ashina Oct 19, 2022
88aa387
refactor(apple): fix type error (#76)
aahna-ashina Oct 19, 2022
4f66809
Merge remote-tracking branch 'upstream/main' into dw-724/refactor-app…
aahna-ashina Oct 19, 2022
2ceb307
Merge branch 'dw-724/refactor-apple-wallet-code' of https://github.co…
aahna-ashina Oct 19, 2022
eaa5568
test(apple): lower coverage threshold (#76)
aahna-ashina Oct 19, 2022
ff8cd3b
Merge branch 'pr/93' into pr/91
aahna-ashina Oct 20, 2022
2b1dada
Revert "Merge branch 'pr/93' into pr/91"
aahna-ashina Oct 20, 2022
ad068c0
docs(apple): update confirmation json
aahna-ashina Oct 20, 2022
09ab583
chore(apple): add template version 4 (#76)
aahna-ashina Oct 20, 2022
55ebf85
Merge branch 'main' into dw-724/refactor-apple-wallet-code
aahna-ashina Oct 24, 2022
8f8b829
refactor(apple): separate apn provider code (#76)
aahna-ashina Oct 24, 2022
2ac3a80
add missing template files
aahna-ashina Oct 24, 2022
11faaae
Update jest.config.js
aahna-ashina Oct 24, 2022
faad5e6
Update Config.ts
aahna-ashina Oct 24, 2022
59c28e6
test(apple): add unit test for v4 of the template
aahna-ashina Oct 24, 2022
bbfa591
refactor(apple): separate apn provider code (#76)
aahna-ashina Oct 25, 2022
138492a
test(apple): mock apn provider during unit tests (#76)
aahna-ashina Oct 25, 2022
a62f5a1
Merge branch 'main' into dw-724/refactor-apple-wallet-code
aahna-ashina Oct 26, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion server/PUSH_NOTIFICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@ These are the steps for pushing a notification to registered passes:
1. Push the latest update to registered passes:
- Go to https://passports.nation3.org/api/pushLastUpdate
- Type the username and password
- Expect this confirmation message: `{"message":"OK: Sent notification request for <number> registered passes"}`
- Expect this confirmation message:
```json
{
"summary": "1 sent, 3 failed",
"sent": [ ... ],
"failed": [ ... ]
}
```
- Wait a few seconds, and the updates passes (and a push notification) should appear on the iOS device of each citizen

## FAQ
Expand Down
2 changes: 1 addition & 1 deletion server/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module.exports = {
testEnvironment: 'node',
coverageThreshold: {
global: {
lines: 80.00
lines: 85.00
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💪

}
}
}
1,845 changes: 731 additions & 1,114 deletions server/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"cypress": "^10.9.0",
"dotenv": "^16.0.2",
"eslint-config-prettier": "^8.5.0",
"jest": "^29.2.2",
"start-server-and-test": "^1.14.0",
"ts-jest": "^29.0.3"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { config } from '../../../../../../../utils/Config'
import { supabase } from '../../../../../../../utils/SupabaseClient'
import { DateUtils } from '../../../../../../../utils/DateUtils'

/**
* Get the List of Updatable Passes. Implementation of
Expand Down Expand Up @@ -28,79 +28,115 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) {
console.log('passesUpdatedSince:', passesUpdatedSince)

// Lookup the serial numbers for the given device
supabase
.from('registrations')
.select('serial_number')
.eq('device_library_identifier', deviceLibraryIdentifier)
.then((result: any) => {
console.log('result:', result)
if (result.error) {
res.status(500).json({
error: 'Internal Server Error: ' + result.error.message
})
} else {
// Convert from [{serial_number:333},{serial_number:444}] to ["333","444"]
let serialNumbers : string[] = []
for (const index in result.data) {
const serialNumber : string = result.data[index]['serial_number']
serialNumbers[Number(index)] = String(serialNumber)
}
console.log('serialNumbers:\n', serialNumbers)

if (serialNumbers.length == 0) {
// There are no matching passes
res.status(204).end()
} else {
// Lookup the latest update and its timestamp
supabase
.from('latest_updates')
.select('*')
.order('time', { ascending: false })
.limit(1)
.single()
.then((latest_updates_result: any) => {
console.log('latest_updates_result:', latest_updates_result)
if (latest_updates_result.error) {
res.status(500).json({
error: 'Internal Server Error: ' + latest_updates_result.error.message
})
} else {
// Convert from ISO string to Date
const latestUpdateDate: Date = new Date(latest_updates_result.data['time'])
console.log('latestUpdateDate:', latestUpdateDate)

if (!passesUpdatedSince) {
// The passes on this device have not been updated previously, so return all passes.
res.status(200).json({
serialNumbers: serialNumbers,
lastUpdated: String(Math.round(latestUpdateDate.getTime() / 1000))
})
} else {
// The passes on this device have been updated previously, so only return passes that
// were updated before the most recent Nation3 update in the `latest_updates` database table.
lookupSerialNumbersFromDatabase(deviceLibraryIdentifier)
.then((serialNumbers: string[]) => {
console.log('then, serialNumbers:', serialNumbers)
if (serialNumbers.length == 0) {
// There are no matching passes
res.status(204).end()
} else {
// Lookup the latest update and its timestamp
lookupTimeOfLatestUpdate()
.then((latestUpdateDate: Date) => {
console.log('then, latestUpdateDate:', latestUpdateDate)
if (!passesUpdatedSince) {
// The passes on this device have not been updated previously, so return all passes.
res.status(200).json({
serialNumbers: serialNumbers,
lastUpdated: String(DateUtils.getTimeInSeconds(latestUpdateDate))
})
} else {
// The passes on this device have been updated previously, so only return passes that
// were updated before the most recent Nation3 update in the `latest_updates` database table.

// Convert from epoch timestamp string to Date
const passesUpdatedSinceDate: Date = new Date(Number(passesUpdatedSince) * 1000)
console.log('passesUpdatedSinceDate:', passesUpdatedSinceDate)

if (passesUpdatedSinceDate.getTime() < latestUpdateDate.getTime()) {
res.status(200).json({
serialNumbers: serialNumbers,
lastUpdated: String(Math.round(latestUpdateDate.getTime() / 1000))
})
} else {
res.status(204).end()
}
}
}
// Convert from epoch timestamp string ('1662889385') to Date
const passesUpdatedSinceDate: Date = DateUtils.getDate(Number(passesUpdatedSince))
console.log('passesUpdatedSinceDate:', passesUpdatedSinceDate)

if (passesUpdatedSinceDate.getTime() < latestUpdateDate.getTime()) {
res.status(200).json({
serialNumbers: serialNumbers,
lastUpdated: String(DateUtils.getTimeInSeconds(latestUpdateDate))
})
}
}
} else {
res.status(204).end()
}
}
})
.catch((error: any) => {
console.log('catch, error:', error)
res.status(500).json({
error: 'Internal Server Error: ' + error.message
})
})
}
})
.catch((error: any) => {
console.log('catch, error:', error)
res.status(500).json({
error: 'Internal Server Error: ' + error.message
})
})
} catch (err: any) {
console.error('[passTypeIdentifier].ts err:\n', err)
res.status(400).json({
error: 'Request Not Authorized: ' + err.message
})
}
}

/**
* Makes a database query for fetching the serial numbers of passes stored in a device.
*
* @param deviceLibraryIdentifier The identifier of the iOS device stored during pass registration
* @returns A Promise with a string array of serial numbers
*/
const lookupSerialNumbersFromDatabase = async (deviceLibraryIdentifier: any): Promise<string[]> => {
console.log('lookupSerialNumbersFromDatabase')

const { data, error } = await supabase.from('registrations').select('serial_number').eq('device_library_identifier', deviceLibraryIdentifier)
console.log('data:', data)
console.log('error:', error)

const promise: Promise<string[]> = new Promise((resolve, reject) => {
if (error) {
reject(error)
} else {
// Convert from [{serial_number:333},{serial_number:444}] to ["333","444"]
let serialNumbers : string[] = []
for (const index in data) {
const serialNumber : string = data[index]['serial_number']
serialNumbers[Number(index)] = String(serialNumber)
}

resolve(serialNumbers)
}
})
return promise
}

/**
* Makes a database query for fetching the most recent update, and then returns its timestamp (Date).
*
* @returns A Promise with a Date
*/
const lookupTimeOfLatestUpdate = async (): Promise<Date> => {
console.log('lookupTimeOfLatestUpdate')

const { data, error } = await supabase.from('latest_updates').select('*').order('time', { ascending: false }).limit(1).single()
console.log('data:', data)
console.log('error:', error)

const promise: Promise<Date> = new Promise((resolve, reject) => {
if (error) {
reject(error)
} else {
// Convert from ISO string ('2022-09-30T12:12:17') to Date
const latestUpdateDate: Date = new Date(data['time'])
console.log('latestUpdateDate:', latestUpdateDate)

resolve(latestUpdateDate)
}
})
return promise
}
16 changes: 8 additions & 8 deletions server/pages/api/pushLastUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ import { Passes } from '../../utils/Passes'

// req = HTTP incoming message, res = HTTP server response
export default function handler(req: NextApiRequest, res: NextApiResponse) {
console.log('/api/pushLastUpdate')
console.info('[pushLastUpdate.ts] handler')

Passes.notifyPassesAboutLastUpdate(Platform.Apple)
.then((result: string) => {
console.log('then, result:', result)
res.status(200).json({
message: 'OK: ' + result
})
.then((result: any) => {
console.info('[pushLastUpdate.ts] then, result:', result)
res.status(200).json(
JSON.parse(result)
)
})
.catch((result: string) => {
console.log('catch, result:', result)
.catch((result: any) => {
console.error('[pushLastUpdate.ts] catch, result:', result)
res.status(500).json({
error: 'Internal Server Error: ' + result
})
Expand Down
22 changes: 22 additions & 0 deletions server/utils/APNProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import apn, { Responses } from 'apn'
import { config } from './Config'

/**
* Apple Push Notification (APN) provider.
*/
export class APNProvider {

static async sendNotification(pushToken: string): Promise<Responses> {
console.info('[APNProvider.ts] sendNotification')

const apnProvider: apn.Provider = new apn.Provider({
cert: `-----BEGIN CERTIFICATE-----\n${config.appleCertificatePEM}\n-----END CERTIFICATE-----`,
key: `-----BEGIN RSA PRIVATE KEY-----\n${config.appleCertificateKey}\n-----END RSA PRIVATE KEY-----`,
production: true
})
const notification: apn.Notification = new apn.Notification();
notification.topic = 'pass.org.passport.nation3'
console.info('[APNProvider.ts] Sending notification...')
return await apnProvider.send(notification, pushToken)
}
}
36 changes: 36 additions & 0 deletions server/utils/DateUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, expect, test } from '@jest/globals'
import { DateUtils } from './DateUtils'

describe('getTimeInSeconds', () => {
test('getTimeInSeconds - 2022-09-30T04:12:17Z', () => {
const date: Date = new Date('2022-09-30T04:12:17Z')
console.log('date:', date)
expect(DateUtils.getTimeInSeconds(date)).toBe(1664511137)
})

test('getTimeInSeconds - 2022-09-30T04:12:17.000Z', () => {
const date: Date = new Date('2022-09-30T04:12:17.000Z')
console.log('date:', date)
expect(DateUtils.getTimeInSeconds(date)).toBe(1664511137)
})

test('getTimeInSeconds - 2022-09-30T04:12:17.499Z', () => {
const date: Date = new Date('2022-09-30T04:12:17.499Z')
console.log('date:', date)
expect(DateUtils.getTimeInSeconds(date)).toBe(1664511137)
})

test('getTimeInSeconds - 2022-09-30T04:12:17.500Z', () => {
const date: Date = new Date('2022-09-30T04:12:17.500Z')
console.log('date:', date)
expect(DateUtils.getTimeInSeconds(date)).toBe(1664511138)
})
})

describe('getDate', () => {
test('getDate - 1662889385', () => {
const date: Date = DateUtils.getDate(1662889385)
console.log('date:', date)
expect(date.toISOString()).toBe('2022-09-11T09:43:05.000Z')
})
})
25 changes: 25 additions & 0 deletions server/utils/DateUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export class DateUtils {

/**
* Calculates a Date's epoch timestamp in seconds. Rounds to the nearest integer to avoid decimals.
*/
static getTimeInSeconds(date: Date): number {
console.log('getTimeInSeconds')
const timeInMilliseconds: number = date.getTime()
const timeInSeconds: number = timeInMilliseconds / 1000
const timeInSecondsRoundedToNearestInteger: number = Math.round(timeInSeconds)
return timeInSecondsRoundedToNearestInteger
}

/**
* Converts from an epoch timestamp (e.g. 1662889385) to Date.
*
* @param timeInSeconds The UNIX timestamp in seconds, e.g. 1662889385.
*/
static getDate(timeInSeconds: number): Date {
console.log('getDate')
const timeInMilliseconds: number = timeInSeconds * 1000
const date: Date = new Date(timeInMilliseconds)
return date
}
}
Loading