Skip to content

Commit

Permalink
Enx ios migration (#973)
Browse files Browse the repository at this point in the history
* add test notification
* formatting
* bad scoping
* formatting
* identifier
* closure
* typo
* add additional logic for enx notification
* typo
* remove active check
* move enx notification to exposure
* modify region and set to pull off env, change styling
* fix os recognition of enx component
* verbage
* add 3 count to prevent showing notification more in 24 hour period
* typo
* bad math operator
* removed constant
* move condition
* move variable
* test
* set a redirect post enx
* Bump mobile resources
* Bumping mobile resources
* Update verbiage for migration
* Bumping mobile resources
* Updating verbiage on migration alerts
* Removing app name from notification
* Bumping mobile resources
* set to 3 times to notify
  • Loading branch information
mxMarkowitz committed Apr 6, 2022
1 parent 2f717a9 commit 634d6af
Show file tree
Hide file tree
Showing 16 changed files with 218 additions and 6 deletions.
1 change: 1 addition & 0 deletions ios/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
[[ExposureManager shared] registerExposureDetectionBackgroundTask];
[[ExposureManager shared] registerChaffBackgroundTask];
[[ExposureManager shared] registerDeleteOldExposuresBackgroundTask];
[[ExposureManager shared] registerEnxMigrationBackgroundTask];

[RNSplashScreen showSplash:@"LaunchScreen" inRootView:rootView];

Expand Down
70 changes: 67 additions & 3 deletions ios/BT/ExposureManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ final class ExposureManager: NSObject {
private static let chaffBackgroundTaskIdentifier = "\(Bundle.main.bundleIdentifier!).chaff"
private static let exposureDetectionBackgroundTaskIdentifier = "\(Bundle.main.bundleIdentifier!).exposure-notification"
private static let deleteOldExposuresBackgroundTaskIdentifier = "\(Bundle.main.bundleIdentifier!).delete-old-exposures"
private static let enxMigrationBackgroundTaskIdentifier = "\(Bundle.main.bundleIdentifier!).enx-migration"

@objc private(set) static var shared: ExposureManager?

Expand Down Expand Up @@ -100,6 +101,13 @@ final class ExposureManager: NSObject {
name: .ChaffRequestTriggered,
object: nil
)

notificationCenter.addObserver(
self,
selector: #selector(scheduleEnxBackgroundTaskIfNeeded),
name: .EnxNotificationTriggered,
object: nil
)
}

deinit {
Expand Down Expand Up @@ -183,6 +191,38 @@ final class ExposureManager: NSObject {
return btSecureStorage.deleteSymptomLogEntries()
}

//Notifies the user they need to migrate
func notifyUserEnxIfNeeded() {
let defaults = UserDefaults.standard
let lastEnxTimestamp = defaults.double(forKey: "lastEnxTimestamp")
let enxCount = defaults.double(forKey: "enxCount")
let sameDay = self.hasBeenTwentyFourHours(lastSubmitted: lastEnxTimestamp)

if (lastEnxTimestamp == 0 || sameDay != nil) {
if (enxCount <= 3) {
let newDate = Date.init();
defaults.set(enxCount + 1, forKey: "enxCount");
defaults.set(newDate, forKey: "lastEnxTimestamp");

let identifier = String.enxMigrationIdentifier
let content = UNMutableNotificationContent()
content.title = String.enxMigrationNotificationTitle.localized
content.body = String.enxMigrationNotificationContent.localized
content.userInfo = [String.notificationUrlKey: "\(String.notificationUrlBasePath)"]
content.sound = .default

let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
userNotificationCenter.add(request) { error in
DispatchQueue.main.async {
if let error = error {
print("Error showing error user notification: \(error)")
}
}
}
}
}
}

///Notifies the user to enable bluetooth to be able to exchange keys
func notifyUserBlueToothOffIfNeeded() {
let identifier = String.bluetoothNotificationIdentifier
Expand Down Expand Up @@ -222,6 +262,17 @@ final class ExposureManager: NSObject {
}, callback: callback)
}

@objc func registerEnxMigrationBackgroundTask() {
bgTaskScheduler.register(forTaskWithIdentifier: ExposureManager.enxMigrationBackgroundTaskIdentifier,
using: .main) { [weak self] task in
//let state = UIApplication.shared.applicationState
//if state == .background || state == .inactive {
// background
self?.scheduleEnxBackgroundTaskIfNeeded()
//}
}
}


// MARK: == Exposure Detection ==

Expand All @@ -236,6 +287,8 @@ final class ExposureManager: NSObject {
// Notify the user if bluetooth is off
strongSelf.notifyUserBlueToothOffIfNeeded()

strongSelf.notifyUserEnxIfNeeded()

// Perform the exposure detection
let progress = strongSelf.detectExposures { result in
switch result {
Expand Down Expand Up @@ -277,7 +330,7 @@ final class ExposureManager: NSObject {
let randomNum = Int.random(in: 0..<20)
let lastChaffTimestamp = defaults.double(forKey: "lastChaffTimestamp")

if ((lastChaffTimestamp == 0 || ((self?.hasBeenTwentyFourHours(lastSubmittedChaff: lastChaffTimestamp)) != nil)) && (randomNum > 8 && randomNum < 19 )) {
if ((lastChaffTimestamp == 0 || ((self?.hasBeenTwentyFourHours(lastSubmitted: lastChaffTimestamp)) != nil)) && (randomNum > 8 && randomNum < 19 )) {
self?.performChaffRequest()
}
}
Expand All @@ -304,12 +357,23 @@ final class ExposureManager: NSObject {
/**
Checks to see if it has been twenty four hours since the last chaff submission.
*/
func hasBeenTwentyFourHours(lastSubmittedChaff: Double) -> Bool {
let timeComparison = Date.init(timeIntervalSinceNow: lastSubmittedChaff)
func hasBeenTwentyFourHours(lastSubmitted: Double) -> Bool {
let timeComparison = Date.init(timeIntervalSinceNow: lastSubmitted)
let twentyFourHoursAgo = Date.init(timeIntervalSinceNow: -3600 * 24)
return timeComparison >= twentyFourHoursAgo
}

@objc func scheduleEnxBackgroundTaskIfNeeded() {
guard manager.exposureNotificationStatus == .active else { return }
let taskRequest = BGProcessingTaskRequest(identifier: ExposureManager.enxMigrationBackgroundTaskIdentifier)
taskRequest.requiresNetworkConnectivity = false
do {
try bgTaskScheduler.submit(taskRequest)
} catch {
print("Unable to schedule background task: \(error)")
}
}

@objc func scheduleExposureDetectionBackgroundTaskIfNeeded() {
guard manager.exposureNotificationStatus == .active else { return }
let taskRequest = BGProcessingTaskRequest(identifier: ExposureManager.exposureDetectionBackgroundTaskIdentifier)
Expand Down
1 change: 1 addition & 0 deletions ios/BT/Extensions/Foundation/Notification+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ extension Notification.Name {
public static let revisionTokenDidChange = Notification.Name(rawValue: "onRevisionTokenDidChange")
public static let ExposuresDidChange = Notification.Name(rawValue: "onExposureRecordUpdated")
public static let ChaffRequestTriggered = Notification.Name(rawValue: "onChaffRequestTriggered")
public static let EnxNotificationTriggered = Notification.Name(rawValue: "onEnxNotificationTriggered")
public static let ExposureNotificationStatusDidChange = Notification.Name(rawValue: "onEnabledStatusUpdated")
public static let remainingDailyFileProcessingCapacityDidChange = Notification.Name(rawValue: "remainingDailyFileProcessingCapacityDidChange")
public static let UrlOfMostRecentlyDetectedKeyFileDidChange = Notification.Name(rawValue: "UrlOfMostRecentlyDetectedKeyFileDidChange")
Expand Down
3 changes: 3 additions & 0 deletions ios/BT/Extensions/Foundation/String+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ extension String {
static let notificationUrlKey = "url"
static let notificationUrlBasePath = "pathcheck://"
static let notificationUrlExposureHistoryPath = "exposureHistory"
static let enxMigrationIdentifier = "enx-migration-notification"
static let enxMigrationNotificationTitle = "Action needed"
static let enxMigrationNotificationContent = "This app will be discontinued soon. To stay protected from COVID-19, open the app to migrate over to Apple's Exposure Notifications."

// JS Layer
static let genericSuccess = "success"
Expand Down
4 changes: 3 additions & 1 deletion ios/BT/bridge/ExposureEventEmitter.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
NSString *const onChaffRequestTriggered = @"onChaffRequestTriggered";
NSString *const onEnabledStatusUpdated = @"onEnabledStatusUpdated";
NSString *const onExposuresChanged = @"onExposureRecordUpdated";
NSString *const onEnxNotificationTriggered = @"onEnxNotificationTriggered";

@interface ExposureEventEmitter : RCTEventEmitter <RCTBridgeModule>
@end
Expand Down Expand Up @@ -32,7 +33,8 @@ - (instancetype)init
return @[
onExposuresChanged,
onEnabledStatusUpdated,
onChaffRequestTriggered
onChaffRequestTriggered,
onEnxNotificationTriggered
];
}

Expand Down
2 changes: 1 addition & 1 deletion mobile_resources_commit
Original file line number Diff line number Diff line change
@@ -1 +1 @@
35462ce37acd66083c897fc3014cc89d09c231ff
9466165c85fc4c1499b912932c41289b5d1ced5d
15 changes: 14 additions & 1 deletion src/AffectedUserFlow/CodeInput/CodeInputScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { FunctionComponent } from "react"
import { View, StyleSheet } from "react-native"
import { Linking, View, StyleSheet } from "react-native"
import { RouteProp, useNavigation, useRoute } from "@react-navigation/native"

import { usePermissionsContext } from "../../Device/PermissionsContext"
Expand All @@ -8,6 +8,8 @@ import EnableExposureNotifications from "./EnableExposureNotifications"
import { applyHeaderLeftBackButton } from "../../navigation/HeaderLeftBackButton"
import { useAffectedUserContext } from "../AffectedUserContext"
import { AffectedUserFlowStackParamList } from "../../navigation/AffectedUserFlowStack"
import { useConfigurationContext } from "../../configuration"
import * as NativeModule from "../../gaen/nativeModule"

import { Colors } from "../../styles"

Expand All @@ -21,11 +23,22 @@ const CodeInputScreen: FunctionComponent = () => {
const route = useRoute<CodeInputScreenRouteProp>()
const { navigateOutOfStack, setLinkCode } = useAffectedUserContext()
const { exposureNotifications } = usePermissionsContext()
const { enxRegion } = useConfigurationContext()
const requestExposureNotifications = async () => {
if (linkCode) {
const response = await NativeModule.requestAuthorization()

if (response.kind === "failure" && response.error === "Restricted") {
Linking.openURL(`ens://v?c=${linkCode}&r=${enxRegion}`)
}
}
}

const linkCode: string | undefined = route?.params?.c || route?.params?.code

if (linkCode) {
setLinkCode(linkCode)
requestExposureNotifications()
navigation.setOptions({
headerLeft: applyHeaderLeftBackButton(navigateOutOfStack),
})
Expand Down
3 changes: 3 additions & 0 deletions src/ExposureContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ const ExposureProvider: FunctionComponent = ({ children }) => {
getLastExposureDetectionDate()

detectExposures()

//Enx migration subscription
//const EnxMigrationInfo = NativeModule.subscribeToEnxMigrationEvents()
/*
// Chaff subscription
const chaffSubscription = NativeModule.subscribeToChaffRequestEvents(() => {
Expand Down
97 changes: 97 additions & 0 deletions src/Home/EnxMigrationInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, { FunctionComponent } from "react"
import {
Linking,
Alert,
TouchableOpacity,
Image,
View,
StyleSheet,
} from "react-native"
import { useTranslation } from "react-i18next"
import { Text } from "../components"
import Logger from "../logger"
import {
Spacing,
Colors,
Typography,
Outlines,
Iconography,
Affordances,
} from "../styles"
import { Icons, Images } from "../assets"
import { SvgXml } from "react-native-svg"

interface EnxMigrationInfoProps {
enxRegion: string
}

const EnxMigrationInfo: FunctionComponent<EnxMigrationInfoProps> = ({
enxRegion,
}) => {
const { t } = useTranslation()
const onboardingUrl = `ens://onboarding?r=${enxRegion}`

const handleOnPress = async () => {
try {
await Linking.openURL(onboardingUrl)
} catch (e) {
Logger.error("Failed to open enx onboarding link: ", { onboardingUrl })
const alertMessage = t("home.could_not_open_link", {
url: onboardingUrl,
})
Alert.alert(alertMessage)
}
}

return (
<TouchableOpacity
style={style.shareContainer}
onPress={handleOnPress}
accessibilityLabel={t("home.migrate_enx")}
>
<View style={style.imageContainer}>
<Image source={Images.ExclamationInCircle} style={style.image} />
</View>
<View style={style.textContainer}>
<Text style={style.shareText}>{t("home.migrate_enx")}</Text>
</View>
<SvgXml
xml={Icons.ChevronRight}
fill={Colors.neutral.shade75}
width={Iconography.xxSmall}
height={Iconography.xxSmall}
/>
</TouchableOpacity>
)
}

const style = StyleSheet.create({
shareContainer: {
...Affordances.floatingContainer,
paddingVertical: Spacing.small,
flexDirection: "row",
alignItems: "center",
borderColor: Colors.accent.danger100,
borderWidth: Outlines.thin,
backgroundColor: Colors.accent.danger25,
},
imageContainer: {
alignItems: "center",
justifyContent: "center",
},
image: {
width: Iconography.large,
height: Iconography.large,
},
textContainer: {
flex: 1,
marginLeft: Spacing.medium,
},
shareText: {
...Typography.body.x30,
...Typography.style.medium,
color: Colors.text.primary,
},
})

export default EnxMigrationInfo
8 changes: 8 additions & 0 deletions src/Home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Image,
Pressable,
Linking,
Platform,
} from "react-native"
import env from "react-native-config"
import { useTranslation } from "react-i18next"
Expand All @@ -32,6 +33,7 @@ import CovidDataWebViewLink from "./CovidDataWebViewLink"
import CallEmergencyServices from "./CallEmergencyServices"
import FaqButton from "./FaqButton"
import { usePermissionsContext } from "../Device/PermissionsContext"
import EnxMigrationInfo from "./EnxMigrationInfo"

import { Icons, Images } from "../assets"
import {
Expand Down Expand Up @@ -63,8 +65,13 @@ const Home: FunctionComponent = () => {
externalTravelGuidanceLink,
healthAuthorityHealthCheckUrl,
verificationStrategy,
enxRegion,
} = useConfigurationContext()

const enxComponent = Platform.OS === "ios" && (
<EnxMigrationInfo enxRegion={enxRegion} />
)

return (
<>
<StatusBar backgroundColor={Colors.background.primaryLight} />
Expand All @@ -74,6 +81,7 @@ const Home: FunctionComponent = () => {
>
<Text style={style.headerText}>{t("screen_titles.home")}</Text>
<NotificationsOff />
{enxComponent}
<ExposureDetectionStatusCard />
{displayCovidData && <CovidDataCard />}
{verificationStrategy === "Simple" ? (
Expand Down
1 change: 1 addition & 0 deletions src/configuration/configurationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const initialState: Configuration = {
displayAgeVerification: false,
emergencyPhoneNumber: "",
enableProductAnalytics: false,
enxRegion: "",
externalCovidDataLabel: "home.covid_data",
externalCovidDataLink: null,
externalTravelGuidanceLink: null,
Expand Down
1 change: 1 addition & 0 deletions src/configuration/configurationInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface Configuration {
displayAgeVerification: boolean
enableProductAnalytics: boolean
emergencyPhoneNumber: string
enxRegion: string
externalCovidDataLink: string | null // link to show external covid data link on main page
externalCovidDataLabel: string // custom label for external covid data, will default to "Covid Data"
externalTravelGuidanceLink: string | null // link to external travel guidence link on main page
Expand Down
4 changes: 4 additions & 0 deletions src/configuration/configurationProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ const ConfigurationProvider: FunctionComponent = ({ children }) => {
const displayQuarantineRecommendation =
env.DISPLAY_QUARANTINE_RECOMMENDATION === "true"

// Enx migration
const enxRegion = env.ENX_REGION || ""

return (
<ConfigurationContext.Provider
value={{
Expand All @@ -113,6 +116,7 @@ const ConfigurationProvider: FunctionComponent = ({ children }) => {
displaySelfAssessment,
displayAgeVerification,
enableProductAnalytics,
enxRegion,
emergencyPhoneNumber,
externalCovidDataLabel,
externalCovidDataLink,
Expand Down
1 change: 1 addition & 0 deletions src/factories/configurationContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default Factory.define<Configuration>(() => ({
displaySelfAssessment: false,
displayAgeVerification: false,
enableProductAnalytics: false,
enxRegion: "",
externalCovidDataLabel: "home.covid_data",
externalCovidDataLink: null,
externalTravelGuidanceLink: null,
Expand Down
Loading

0 comments on commit 634d6af

Please sign in to comment.