Skip to content

Commit 634d6af

Browse files
authored
Enx ios migration (#973)
* 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
1 parent 2f717a9 commit 634d6af

File tree

16 files changed

+218
-6
lines changed

16 files changed

+218
-6
lines changed

ios/AppDelegate.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
5757
[[ExposureManager shared] registerExposureDetectionBackgroundTask];
5858
[[ExposureManager shared] registerChaffBackgroundTask];
5959
[[ExposureManager shared] registerDeleteOldExposuresBackgroundTask];
60+
[[ExposureManager shared] registerEnxMigrationBackgroundTask];
6061

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

ios/BT/ExposureManager.swift

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ final class ExposureManager: NSObject {
4444
private static let chaffBackgroundTaskIdentifier = "\(Bundle.main.bundleIdentifier!).chaff"
4545
private static let exposureDetectionBackgroundTaskIdentifier = "\(Bundle.main.bundleIdentifier!).exposure-notification"
4646
private static let deleteOldExposuresBackgroundTaskIdentifier = "\(Bundle.main.bundleIdentifier!).delete-old-exposures"
47+
private static let enxMigrationBackgroundTaskIdentifier = "\(Bundle.main.bundleIdentifier!).enx-migration"
4748

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

@@ -100,6 +101,13 @@ final class ExposureManager: NSObject {
100101
name: .ChaffRequestTriggered,
101102
object: nil
102103
)
104+
105+
notificationCenter.addObserver(
106+
self,
107+
selector: #selector(scheduleEnxBackgroundTaskIfNeeded),
108+
name: .EnxNotificationTriggered,
109+
object: nil
110+
)
103111
}
104112

105113
deinit {
@@ -183,6 +191,38 @@ final class ExposureManager: NSObject {
183191
return btSecureStorage.deleteSymptomLogEntries()
184192
}
185193

194+
//Notifies the user they need to migrate
195+
func notifyUserEnxIfNeeded() {
196+
let defaults = UserDefaults.standard
197+
let lastEnxTimestamp = defaults.double(forKey: "lastEnxTimestamp")
198+
let enxCount = defaults.double(forKey: "enxCount")
199+
let sameDay = self.hasBeenTwentyFourHours(lastSubmitted: lastEnxTimestamp)
200+
201+
if (lastEnxTimestamp == 0 || sameDay != nil) {
202+
if (enxCount <= 3) {
203+
let newDate = Date.init();
204+
defaults.set(enxCount + 1, forKey: "enxCount");
205+
defaults.set(newDate, forKey: "lastEnxTimestamp");
206+
207+
let identifier = String.enxMigrationIdentifier
208+
let content = UNMutableNotificationContent()
209+
content.title = String.enxMigrationNotificationTitle.localized
210+
content.body = String.enxMigrationNotificationContent.localized
211+
content.userInfo = [String.notificationUrlKey: "\(String.notificationUrlBasePath)"]
212+
content.sound = .default
213+
214+
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
215+
userNotificationCenter.add(request) { error in
216+
DispatchQueue.main.async {
217+
if let error = error {
218+
print("Error showing error user notification: \(error)")
219+
}
220+
}
221+
}
222+
}
223+
}
224+
}
225+
186226
///Notifies the user to enable bluetooth to be able to exchange keys
187227
func notifyUserBlueToothOffIfNeeded() {
188228
let identifier = String.bluetoothNotificationIdentifier
@@ -222,6 +262,17 @@ final class ExposureManager: NSObject {
222262
}, callback: callback)
223263
}
224264

265+
@objc func registerEnxMigrationBackgroundTask() {
266+
bgTaskScheduler.register(forTaskWithIdentifier: ExposureManager.enxMigrationBackgroundTaskIdentifier,
267+
using: .main) { [weak self] task in
268+
//let state = UIApplication.shared.applicationState
269+
//if state == .background || state == .inactive {
270+
// background
271+
self?.scheduleEnxBackgroundTaskIfNeeded()
272+
//}
273+
}
274+
}
275+
225276

226277
// MARK: == Exposure Detection ==
227278

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

290+
strongSelf.notifyUserEnxIfNeeded()
291+
239292
// Perform the exposure detection
240293
let progress = strongSelf.detectExposures { result in
241294
switch result {
@@ -277,7 +330,7 @@ final class ExposureManager: NSObject {
277330
let randomNum = Int.random(in: 0..<20)
278331
let lastChaffTimestamp = defaults.double(forKey: "lastChaffTimestamp")
279332

280-
if ((lastChaffTimestamp == 0 || ((self?.hasBeenTwentyFourHours(lastSubmittedChaff: lastChaffTimestamp)) != nil)) && (randomNum > 8 && randomNum < 19 )) {
333+
if ((lastChaffTimestamp == 0 || ((self?.hasBeenTwentyFourHours(lastSubmitted: lastChaffTimestamp)) != nil)) && (randomNum > 8 && randomNum < 19 )) {
281334
self?.performChaffRequest()
282335
}
283336
}
@@ -304,12 +357,23 @@ final class ExposureManager: NSObject {
304357
/**
305358
Checks to see if it has been twenty four hours since the last chaff submission.
306359
*/
307-
func hasBeenTwentyFourHours(lastSubmittedChaff: Double) -> Bool {
308-
let timeComparison = Date.init(timeIntervalSinceNow: lastSubmittedChaff)
360+
func hasBeenTwentyFourHours(lastSubmitted: Double) -> Bool {
361+
let timeComparison = Date.init(timeIntervalSinceNow: lastSubmitted)
309362
let twentyFourHoursAgo = Date.init(timeIntervalSinceNow: -3600 * 24)
310363
return timeComparison >= twentyFourHoursAgo
311364
}
312365

366+
@objc func scheduleEnxBackgroundTaskIfNeeded() {
367+
guard manager.exposureNotificationStatus == .active else { return }
368+
let taskRequest = BGProcessingTaskRequest(identifier: ExposureManager.enxMigrationBackgroundTaskIdentifier)
369+
taskRequest.requiresNetworkConnectivity = false
370+
do {
371+
try bgTaskScheduler.submit(taskRequest)
372+
} catch {
373+
print("Unable to schedule background task: \(error)")
374+
}
375+
}
376+
313377
@objc func scheduleExposureDetectionBackgroundTaskIfNeeded() {
314378
guard manager.exposureNotificationStatus == .active else { return }
315379
let taskRequest = BGProcessingTaskRequest(identifier: ExposureManager.exposureDetectionBackgroundTaskIdentifier)

ios/BT/Extensions/Foundation/Notification+Extensions.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ extension Notification.Name {
88
public static let revisionTokenDidChange = Notification.Name(rawValue: "onRevisionTokenDidChange")
99
public static let ExposuresDidChange = Notification.Name(rawValue: "onExposureRecordUpdated")
1010
public static let ChaffRequestTriggered = Notification.Name(rawValue: "onChaffRequestTriggered")
11+
public static let EnxNotificationTriggered = Notification.Name(rawValue: "onEnxNotificationTriggered")
1112
public static let ExposureNotificationStatusDidChange = Notification.Name(rawValue: "onEnabledStatusUpdated")
1213
public static let remainingDailyFileProcessingCapacityDidChange = Notification.Name(rawValue: "remainingDailyFileProcessingCapacityDidChange")
1314
public static let UrlOfMostRecentlyDetectedKeyFileDidChange = Notification.Name(rawValue: "UrlOfMostRecentlyDetectedKeyFileDidChange")

ios/BT/Extensions/Foundation/String+Extensions.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ extension String {
4646
static let notificationUrlKey = "url"
4747
static let notificationUrlBasePath = "pathcheck://"
4848
static let notificationUrlExposureHistoryPath = "exposureHistory"
49+
static let enxMigrationIdentifier = "enx-migration-notification"
50+
static let enxMigrationNotificationTitle = "Action needed"
51+
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."
4952

5053
// JS Layer
5154
static let genericSuccess = "success"

ios/BT/bridge/ExposureEventEmitter.m

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
NSString *const onChaffRequestTriggered = @"onChaffRequestTriggered";
66
NSString *const onEnabledStatusUpdated = @"onEnabledStatusUpdated";
77
NSString *const onExposuresChanged = @"onExposureRecordUpdated";
8+
NSString *const onEnxNotificationTriggered = @"onEnxNotificationTriggered";
89

910
@interface ExposureEventEmitter : RCTEventEmitter <RCTBridgeModule>
1011
@end
@@ -32,7 +33,8 @@ - (instancetype)init
3233
return @[
3334
onExposuresChanged,
3435
onEnabledStatusUpdated,
35-
onChaffRequestTriggered
36+
onChaffRequestTriggered,
37+
onEnxNotificationTriggered
3638
];
3739
}
3840

mobile_resources_commit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
35462ce37acd66083c897fc3014cc89d09c231ff
1+
9466165c85fc4c1499b912932c41289b5d1ced5d

src/AffectedUserFlow/CodeInput/CodeInputScreen.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { FunctionComponent } from "react"
2-
import { View, StyleSheet } from "react-native"
2+
import { Linking, View, StyleSheet } from "react-native"
33
import { RouteProp, useNavigation, useRoute } from "@react-navigation/native"
44

55
import { usePermissionsContext } from "../../Device/PermissionsContext"
@@ -8,6 +8,8 @@ import EnableExposureNotifications from "./EnableExposureNotifications"
88
import { applyHeaderLeftBackButton } from "../../navigation/HeaderLeftBackButton"
99
import { useAffectedUserContext } from "../AffectedUserContext"
1010
import { AffectedUserFlowStackParamList } from "../../navigation/AffectedUserFlowStack"
11+
import { useConfigurationContext } from "../../configuration"
12+
import * as NativeModule from "../../gaen/nativeModule"
1113

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

@@ -21,11 +23,22 @@ const CodeInputScreen: FunctionComponent = () => {
2123
const route = useRoute<CodeInputScreenRouteProp>()
2224
const { navigateOutOfStack, setLinkCode } = useAffectedUserContext()
2325
const { exposureNotifications } = usePermissionsContext()
26+
const { enxRegion } = useConfigurationContext()
27+
const requestExposureNotifications = async () => {
28+
if (linkCode) {
29+
const response = await NativeModule.requestAuthorization()
30+
31+
if (response.kind === "failure" && response.error === "Restricted") {
32+
Linking.openURL(`ens://v?c=${linkCode}&r=${enxRegion}`)
33+
}
34+
}
35+
}
2436

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

2739
if (linkCode) {
2840
setLinkCode(linkCode)
41+
requestExposureNotifications()
2942
navigation.setOptions({
3043
headerLeft: applyHeaderLeftBackButton(navigateOutOfStack),
3144
})

src/ExposureContext.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ const ExposureProvider: FunctionComponent = ({ children }) => {
156156
getLastExposureDetectionDate()
157157

158158
detectExposures()
159+
160+
//Enx migration subscription
161+
//const EnxMigrationInfo = NativeModule.subscribeToEnxMigrationEvents()
159162
/*
160163
// Chaff subscription
161164
const chaffSubscription = NativeModule.subscribeToChaffRequestEvents(() => {

src/Home/EnxMigrationInfo.tsx

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import React, { FunctionComponent } from "react"
2+
import {
3+
Linking,
4+
Alert,
5+
TouchableOpacity,
6+
Image,
7+
View,
8+
StyleSheet,
9+
} from "react-native"
10+
import { useTranslation } from "react-i18next"
11+
import { Text } from "../components"
12+
import Logger from "../logger"
13+
import {
14+
Spacing,
15+
Colors,
16+
Typography,
17+
Outlines,
18+
Iconography,
19+
Affordances,
20+
} from "../styles"
21+
import { Icons, Images } from "../assets"
22+
import { SvgXml } from "react-native-svg"
23+
24+
interface EnxMigrationInfoProps {
25+
enxRegion: string
26+
}
27+
28+
const EnxMigrationInfo: FunctionComponent<EnxMigrationInfoProps> = ({
29+
enxRegion,
30+
}) => {
31+
const { t } = useTranslation()
32+
const onboardingUrl = `ens://onboarding?r=${enxRegion}`
33+
34+
const handleOnPress = async () => {
35+
try {
36+
await Linking.openURL(onboardingUrl)
37+
} catch (e) {
38+
Logger.error("Failed to open enx onboarding link: ", { onboardingUrl })
39+
const alertMessage = t("home.could_not_open_link", {
40+
url: onboardingUrl,
41+
})
42+
Alert.alert(alertMessage)
43+
}
44+
}
45+
46+
return (
47+
<TouchableOpacity
48+
style={style.shareContainer}
49+
onPress={handleOnPress}
50+
accessibilityLabel={t("home.migrate_enx")}
51+
>
52+
<View style={style.imageContainer}>
53+
<Image source={Images.ExclamationInCircle} style={style.image} />
54+
</View>
55+
<View style={style.textContainer}>
56+
<Text style={style.shareText}>{t("home.migrate_enx")}</Text>
57+
</View>
58+
<SvgXml
59+
xml={Icons.ChevronRight}
60+
fill={Colors.neutral.shade75}
61+
width={Iconography.xxSmall}
62+
height={Iconography.xxSmall}
63+
/>
64+
</TouchableOpacity>
65+
)
66+
}
67+
68+
const style = StyleSheet.create({
69+
shareContainer: {
70+
...Affordances.floatingContainer,
71+
paddingVertical: Spacing.small,
72+
flexDirection: "row",
73+
alignItems: "center",
74+
borderColor: Colors.accent.danger100,
75+
borderWidth: Outlines.thin,
76+
backgroundColor: Colors.accent.danger25,
77+
},
78+
imageContainer: {
79+
alignItems: "center",
80+
justifyContent: "center",
81+
},
82+
image: {
83+
width: Iconography.large,
84+
height: Iconography.large,
85+
},
86+
textContainer: {
87+
flex: 1,
88+
marginLeft: Spacing.medium,
89+
},
90+
shareText: {
91+
...Typography.body.x30,
92+
...Typography.style.medium,
93+
color: Colors.text.primary,
94+
},
95+
})
96+
97+
export default EnxMigrationInfo

src/Home/index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Image,
88
Pressable,
99
Linking,
10+
Platform,
1011
} from "react-native"
1112
import env from "react-native-config"
1213
import { useTranslation } from "react-i18next"
@@ -32,6 +33,7 @@ import CovidDataWebViewLink from "./CovidDataWebViewLink"
3233
import CallEmergencyServices from "./CallEmergencyServices"
3334
import FaqButton from "./FaqButton"
3435
import { usePermissionsContext } from "../Device/PermissionsContext"
36+
import EnxMigrationInfo from "./EnxMigrationInfo"
3537

3638
import { Icons, Images } from "../assets"
3739
import {
@@ -63,8 +65,13 @@ const Home: FunctionComponent = () => {
6365
externalTravelGuidanceLink,
6466
healthAuthorityHealthCheckUrl,
6567
verificationStrategy,
68+
enxRegion,
6669
} = useConfigurationContext()
6770

71+
const enxComponent = Platform.OS === "ios" && (
72+
<EnxMigrationInfo enxRegion={enxRegion} />
73+
)
74+
6875
return (
6976
<>
7077
<StatusBar backgroundColor={Colors.background.primaryLight} />
@@ -74,6 +81,7 @@ const Home: FunctionComponent = () => {
7481
>
7582
<Text style={style.headerText}>{t("screen_titles.home")}</Text>
7683
<NotificationsOff />
84+
{enxComponent}
7785
<ExposureDetectionStatusCard />
7886
{displayCovidData && <CovidDataCard />}
7987
{verificationStrategy === "Simple" ? (

0 commit comments

Comments
 (0)