diff --git a/.env.dev b/.env.dev index e32566f9..90abf320 100644 --- a/.env.dev +++ b/.env.dev @@ -1,6 +1,7 @@ SSL_PINNING_CERT_NAME=thaialert-dev API_URL=https://api.dev.thaialert.com +NOTIFICATION_API_URL=https://notification.dev.thaialert.com SHOP_QR_PINNING_CERT=shopqr-dev SHOP_API_URL=https://api-dev.covid.odds.team SHOP_API_NAME=morchana-app -SHOP_API_KEY=qWjchJvz5cMRBk3EUeFPBhkUXybUBSaPTkVacsUfVztkzqHRQKZCT \ No newline at end of file +SHOP_API_KEY=qWjchJvz5cMRBk3EUeFPBhkUXybUBSaPTkVacsUfVztkzqHRQKZCT diff --git a/.env.example b/.env.example index d36b80d0..a07f9af5 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ SSL_PINNING_CERT_NAME= API_URL= -API_KEY= \ No newline at end of file +API_KEY= +NOTIFICATION_API_URL= diff --git a/.prettierrc b/.prettierrc index debd4dbe..594f4957 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,6 +2,5 @@ "semi": false, "trailingComma": "all", "singleQuote": true, - "no-func-assign": false, "proseWrap": "always" } diff --git a/android/build.gradle b/android/build.gradle index d751cf2e..9b6d2764 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -15,6 +15,7 @@ buildscript { appCompatVersion = "1.0.2" supportV4Version = "1.0.0" mediaCompatVersion = "1.0.1" + firebaseMessagingVersion = '21.1.0' } repositories { jcenter() @@ -62,4 +63,4 @@ subprojects { } } } -} \ No newline at end of file +} diff --git a/i18n/locales/en.js b/i18n/locales/en.js index ae9498bc..5862530b 100644 --- a/i18n/locales/en.js +++ b/i18n/locales/en.js @@ -80,11 +80,9 @@ export default { settings: 'Settings', risk: 'risk', scan_result: 'scan result', - low_risk: 'Low Risk', data_at: 'data at', por_sor: 'BE', scan_again: 'scan again', - risk_level: 'risk level', wrong_data: 'wrong data', record_contact_and_estimate_risk: 'Record Contact and Estimate Risk', already_registered: 'already registered', @@ -273,4 +271,6 @@ export default { back: 'BACK', change_lang: 'Change Language', beacon_header: 'Found Beacon', + notification_history: 'Notification', + notification_history_empty: 'This list is empty.', } diff --git a/i18n/locales/th.js b/i18n/locales/th.js index a6ba7a83..1f71e2d2 100644 --- a/i18n/locales/th.js +++ b/i18n/locales/th.js @@ -79,11 +79,9 @@ export default { settings: 'ตั้งค่า', risk: 'ความเสี่ยง', scan_result: 'ผลลัพธ์การสแกน', - low_risk: 'เสี่ยงน้อย', data_at: 'ข้อมูลวันที่', por_sor: 'พ.ศ', scan_again: 'สแกนใหม่อีกครั้ง', - risk_level: 'ระดับความเสี่ยง', wrong_data: 'ข้อมูลไม่ถูกต้อง', record_contact_and_estimate_risk: 'เพื่อบันทึกการเข้าใกล้และตรวจสอบความเสี่ยง', @@ -271,4 +269,6 @@ export default { change_lang: 'เปลี่ยนภาษา', back: 'ย้อนกลับ', beacon_header: 'คุณได้พบ Beacon', + notification_history: 'แจ้งเตือน', + notification_history_empty: 'ไม่มีการแจ้งเตือน', } diff --git a/ios/thaialert-dev.cer b/ios/thaialert-dev.cer index b4926fee..fccb7cf8 100644 Binary files a/ios/thaialert-dev.cer and b/ios/thaialert-dev.cer differ diff --git a/src/App.tsx b/src/App.tsx index ec8f9e8c..960b9edd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -37,6 +37,7 @@ class App extends React.Component { state: { loaded: boolean activateCallback?: Function + notificationTriggerNumber?: number } appState: AppStateStatus constructor(props) { @@ -116,6 +117,8 @@ class App extends React.Component { pushNotification.configure(this.onNotification) } onNotification = (notification) => { + this.setState({notificationTriggerNumber:(this.state.notificationTriggerNumber ?? 0) + 1}) + const notificationData = notification?.data?.data || notification?.data if (!notificationData?.type) { return @@ -150,6 +153,7 @@ class App extends React.Component { diff --git a/src/api-notification.ts b/src/api-notification.ts new file mode 100644 index 00000000..fe928313 --- /dev/null +++ b/src/api-notification.ts @@ -0,0 +1,58 @@ +import _ from 'lodash' +import { fetch } from 'react-native-ssl-pinning' +import { getAnonymousHeaders } from './api' +import { NOTIFICATION_API_URL, SSL_PINNING_CERT_NAME } from './config' +import { NotificationHistoryModel } from './navigations/3-MainApp/NotificationHistory' + +export const getNotifications = async (param: { + skip?: number + limit?: number +}) => { + try { + const res = await fetch( + NOTIFICATION_API_URL + + '/notifications' + + (param + ? `?${new URLSearchParams(param as Record)}` + : ''), + { + sslPinning: { + certs: [SSL_PINNING_CERT_NAME], + }, + headers: getAnonymousHeaders(), + method: 'GET', + }, + ) + const json = await res.json() + + if (_.isArray(json)) { + return json as NotificationHistoryModel[] + } + } catch (error) { + // console.error('Failed', json); + } + + return [] +} + +/* +export const patchNotifications = async () => { + try { + const res = await fetch(NOTIFICATION_API_URL + '/notifications', { + sslPinning: { + certs: [SSL_PINNING_CERT_NAME], + }, + headers: getAnonymousHeaders(), + method: 'PATCH', + }) + const text = await res.text() + console.log('patchNotifications', text) + + } catch (error) { + // console.error('Failed', json); + return 'failed' as const + } + + return 'ok' as const +} +*/ diff --git a/src/config.ts b/src/config.ts index a42dfe87..56c1b052 100644 --- a/src/config.ts +++ b/src/config.ts @@ -15,4 +15,7 @@ export const PUBLIC_KEY_PINNING_CERT = Config.PUBLIC_KEY_PINNING_CERT export const SHOP_API_NAME = Config.SHOP_API_NAME export const SHOP_API_KEY = Config.SHOP_API_KEY export const SHOP_API_URL = Config.SHOP_API_URL -export const SHOP_QR_PINNING_CERT = Config.SHOP_QR_PINNING_CERT \ No newline at end of file +export const SHOP_QR_PINNING_CERT = Config.SHOP_QR_PINNING_CERT + +// Notification api config +export const NOTIFICATION_API_URL = Config.NOTIFICATION_API_URL diff --git a/src/navigations/3-MainApp/MainApp/UpdateProfileButton.tsx b/src/navigations/3-MainApp/MainApp/UpdateProfileButton.tsx index fc883349..0e824b01 100644 --- a/src/navigations/3-MainApp/MainApp/UpdateProfileButton.tsx +++ b/src/navigations/3-MainApp/MainApp/UpdateProfileButton.tsx @@ -37,7 +37,7 @@ export const UpdateProfileButton = ({ width, style, onChange }) => { if (daySinceCreated >= 3) { Alert.alert( I18n.t('are_you_sure'), - `I18n.t('after_changed_pic_you_will_not_be_able_to_change_until') ${DEFAULT_PERIODS} I18n.t('day_s_have_passed')`, + `${I18n.t('after_changed_pic_you_will_not_be_able_to_change_until')} ${DEFAULT_PERIODS} ${I18n.t('day_s_have_passed')}`, [ { text: I18n.t('cancel'), style: 'cancel' }, { diff --git a/src/navigations/3-MainApp/MainAppStack.tsx b/src/navigations/3-MainApp/MainAppStack.tsx index 239dc9d3..de5fb694 100644 --- a/src/navigations/3-MainApp/MainAppStack.tsx +++ b/src/navigations/3-MainApp/MainAppStack.tsx @@ -11,7 +11,7 @@ import { MainApp } from './NewMainApp' import { MainAppFaceCamera } from './MainAppFaceCamera' import { QRCodeScan } from './QRCodeScan' import { Settings } from './Settings' - +import { NotificationHistory } from './NotificationHistory' import I18n from '../../../i18n/i18n'; const TabBarLabel = ({ title, focused }: any) => { @@ -76,8 +76,23 @@ export const MainAppTab = createBottomTabNavigator( ), }, }, - // Notification: { - // screen: Settings, + NotificationHistory: { + screen: NotificationHistory, + navigationOptions: { + tabBarLabel: ({ focused }: any) => ( + + ), + tabBarIcon: ({ focused }: any) => ( + + ), + }, + }, + // Debug: { + // screen: Debug, // navigationOptions: { // tabBarLabel: ({ focused }: any) => ( // diff --git a/src/navigations/3-MainApp/NewMainApp/UpdateProfileButton.tsx b/src/navigations/3-MainApp/NewMainApp/UpdateProfileButton.tsx index a6db9748..3274b40c 100644 --- a/src/navigations/3-MainApp/NewMainApp/UpdateProfileButton.tsx +++ b/src/navigations/3-MainApp/NewMainApp/UpdateProfileButton.tsx @@ -37,7 +37,7 @@ export const UpdateProfileButton = ({ width, style, onChange }) => { if (daySinceCreated >= 3) { Alert.alert( I18n.t('are_you_sure'), - `I18n.t('after_changed_pic_you_will_not_be_able_to_change_until') ${DEFAULT_PERIODS} I18n.t('day_s_have_passed')`, + `${I18n.t('after_changed_pic_you_will_not_be_able_to_change_until')} ${DEFAULT_PERIODS} ${I18n.t('day_s_have_passed')}`, [ { text: I18n.t('cancel'), style: 'cancel' }, { diff --git a/src/navigations/3-MainApp/NotificationHistory.tsx b/src/navigations/3-MainApp/NotificationHistory.tsx new file mode 100644 index 00000000..fcfe8d6a --- /dev/null +++ b/src/navigations/3-MainApp/NotificationHistory.tsx @@ -0,0 +1,182 @@ +import moment from 'moment' +import React, { useEffect, useState } from 'react' +import { FlatList, StatusBar, StyleSheet, Text, View } from 'react-native' +import { SafeAreaView } from 'react-native-safe-area-context' +import AntIcon from 'react-native-vector-icons/AntDesign' +import { getNotifications } from '../../api-notification' +import { MyBackground } from '../../components/MyBackground' +import { COLORS, FONT_FAMILY, FONT_SIZES } from '../../styles' +import I18n from '../../../i18n/i18n' +import { ContractTracerContext } from '../../services/contact-tracing-provider' +import { useFocusEffect } from 'react-navigation-hooks' + +export interface NotificationHistoryModel { + title: string + type: string + message: string + sendedAt: string + anonymousId: string + isRead: true +} + +const PAGE_SIZE = 20 +// let cnt = 0 + +export const NotificationHistory = () => { + const [history, setHistory] = useState([]) + const [refreshing, setRefreshing] = useState(false) + + const { notificationTriggerNumber } = React.useContext(ContractTracerContext) + + const historyRef = React.useRef(history) + historyRef.current = history + + useFocusEffect( + React.useCallback(() => { + getNotifications({ skip: 0, limit: PAGE_SIZE }).then(setHistory) + }, []), + ) + + useEffect(() => { + getNotifications({ skip: 0, limit: PAGE_SIZE }).then(setHistory) + }, [notificationTriggerNumber]) + + return ( + + + + ( + + + {I18n.t('notification_history_empty')} + + + )} + refreshing={refreshing} + onRefresh={async () => { + setRefreshing(true) + const notifications = await getNotifications({ + skip: 0, + limit: PAGE_SIZE, + }) + setHistory(notifications) + setRefreshing(false) + }} + onEndReachedThreshold={0.5} + onEndReached={async () => { + const newHistory = await getNotifications({ + skip: historyRef.current.length, + limit: PAGE_SIZE, + }) + if (newHistory.length) { + setHistory(historyRef.current.concat(newHistory)) + } else { + setEndOfList(true) + } + }} + renderItem={({ item, index }) => { + return ( + + + + + + + + {item.title} + + + + {item.message} + + {moment(item.sendedAt) + .format('DD MMM YYYY HH:mm น.') + .toString()} + + + ) + }} + /> + + + ) +} + +const styles = StyleSheet.create({ + safeAreaView: { + flex: 1, + }, + sectionLine: { + padding: 24, + borderBottomWidth: 1, + borderBottomColor: COLORS.GRAY_5, + }, + titleSection: { + flex: 1, + flexDirection: 'row', + }, + titleWarning: { + color: COLORS.RED_WARNING, + fontSize: FONT_SIZES[500], + fontFamily: FONT_FAMILY, + fontWeight: '500', + }, + titleInfo: { + color: COLORS.BLUE_INFO, + fontSize: FONT_SIZES[500], + fontFamily: FONT_FAMILY, + fontWeight: '500', + }, + iconStyle: { + position: 'relative', + top: 4, + paddingRight: 12, + }, + descriptionStyle: { + color: COLORS.BLACK_1, + fontSize: FONT_SIZES[500], + fontFamily: FONT_FAMILY, + }, + dateStyle: { + color: COLORS.GRAY_4, + fontSize: FONT_SIZES[400], + fontFamily: FONT_FAMILY, + }, + emptyTextView: { + flex: 1, + height: '100%', + marginTop: 100, + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + }, + emptyText: { + fontSize: 18, + textAlign: 'center', + textAlignVertical: 'center', + fontWeight: 'bold', + flex: 1, + color: '#A0A4B1', + }, +}) diff --git a/src/services/contact-tracing-provider.tsx b/src/services/contact-tracing-provider.tsx index 1f289898..568df7e2 100644 --- a/src/services/contact-tracing-provider.tsx +++ b/src/services/contact-tracing-provider.tsx @@ -14,6 +14,7 @@ const eventEmitter = new NativeEventEmitter(NativeModules.ContactTracerModule) interface ContactTracerProps { anonymousId: string isPassedOnboarding: boolean + notificationTriggerNumber: number; } interface ContactTracerState { @@ -23,11 +24,12 @@ interface ContactTracerState { anonymousId: string statusText: string beaconLocationName: any + notificationTriggerNumber?: number; enable: () => void disable: () => void } -const Context = React.createContext(null) +export const ContractTracerContext = React.createContext(null) export class ContactTracerProvider extends React.Component< ContactTracerProps, @@ -324,13 +326,13 @@ export class ContactTracerProvider extends React.Component< render() { return ( - + {this.props.children} - + ) } } export const useContactTracer = (): ContactTracerState => { - return useContext(Context) + return useContext(ContractTracerContext) } diff --git a/src/styles.ts b/src/styles.ts index d8fcf8b1..9f297c27 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -1,4 +1,3 @@ -import { normalize } from 'react-native-elements' export const COLORS = { WHITE: '#FAFDFF', @@ -10,6 +9,7 @@ export const COLORS = { GRAY_2: '#A1A4B1', GRAY_3: '#E5E5E5', GRAY_4: '#505565', + GRAY_5: '#E0E0E0', DANGER: '#DF4A4A', BLACK_1: '#1F1D1D', BLACK_2: '#171717', @@ -25,6 +25,8 @@ export const COLORS = { SECONDARY_DIM: '#576675', LIGHT_BLUE: '#FAFDFF', BORDER_LIGHT_BLUE: '#E6F2FA', + RED_WARNING: '#B82020', + BLUE_INFO: '#205DB8' } export const FONT_FAMILY = 'DBHelvethaicaX-Reg' diff --git a/yarn.lock b/yarn.lock index c674457d..81085c1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7972,10 +7972,9 @@ react-native-confirm@^0.0.1: react-native-input-xg "^0.0.2" react-native-stylesheet-xg "^1.1.0" -react-native-contact-tracer@1.2.0: +"react-native-contact-tracer@git://github.com/Morchana/contact-tracer.git": version "1.2.0" - resolved "https://registry.yarnpkg.com/react-native-contact-tracer/-/react-native-contact-tracer-1.2.0.tgz#a9baf0bd5f7b63051519a8d4dd240778edf89832" - integrity sha512-2U1J+w/+r5qdK2jT0fH5OhmHaZSrjLbwWE7B63eeTYrchvDrqVYTVup1EpxhEFHutvsAkFDLbHNt4cO3+vpIzQ== + resolved "git://github.com/Morchana/contact-tracer.git#36e5fb728405318c27d8429569c08d8a65a85b22" react-native-datepicker@^1.4.4: version "1.7.2"