diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d097d2a..14f37a94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## [2.9.18] - 2021-03-09 +### Added +- Fire Analytics events in Pending Family Members popup. +- Added terms and conditions to Pending Family Members popup [#557](https://github.com/rokwire/safer-illinois-app/issues/557). + +### Changed +- Minor text changes in Pending Family Members popup. + +## [2.9.17] - 2021-03-04 +### Added +- Added email or phone in Family Members list entry [#549](https://github.com/rokwire/safer-illinois-app/issues/549). + +### Changed +- Renamed all occurences of "Reject" with "Revoke" in Family Members panel [#553](https://github.com/rokwire/safer-illinois-app/issues/553). + +## [2.9.16] - 2021-03-02 +### Fixed +- Fixed "Remove My Information" processing [#547](https://github.com/rokwire/safer-illinois-app/issues/547). + +## [2.9.15] - 2021-02-26 +### Added +- Implemented PullToRefresh feature in Home panel [#544](https://github.com/rokwire/safer-illinois-app/issues/544). + +### Changed +- Load family member test price from health rules constants [#542](https://github.com/rokwire/safer-illinois-app/issues/542). + +## [2.9.14] - 2021-02-23 +### Added +- Introduced Family members panel [#537](https://github.com/rokwire/safer-illinois-app/issues/537). + +## [2.9.13] - 2021-02-22 +### Changed +- Updated Positive IP & NIP step & explanation strings [#529](https://github.com/rokwire/safer-illinois-app/issues/529). +- Make POSITIVE-NIP COVID-19 PCR test result like the regular "PCR.negative" test result [#532](https://github.com/rokwire/safer-illinois-app/issues/532). +- Introduced Approve Family member panel [#534](https://github.com/rokwire/safer-illinois-app/issues/534). + +### Added +- Added camera usage mention in disclosure screen [#530](https://github.com/rokwire/safer-illinois-app/issues/530). +- Handled family members requests [#534](https://github.com/rokwire/safer-illinois-app/issues/534). + ## [2.9.12] - 2021-02-09 ### Changed - Increased connectivity plugin version [#519](https://github.com/rokwire/safer-illinois-app/issues/519). diff --git a/assets/flexUI.json b/assets/flexUI.json index 6e1c4361..fba00801 100644 --- a/assets/flexUI.json +++ b/assets/flexUI.json @@ -12,7 +12,7 @@ "settings.notifications": ["covid19"], "settings.covid19": ["exposure_notifications", "provider_test_result", "qr_code"], "settings.privacy": ["statement"], - "settings.account": ["personal_info"], + "settings.account": ["personal_info", "family_members"], "info_center": ["stay_healthy", "your_health"], "info_center.stay_healthy" : ["recent_event", "next_step", "symptom_checkin", "add_test_result"], diff --git a/assets/health.rules.json b/assets/health.rules.json index e53af67b..014d6862 100644 --- a/assets/health.rules.json +++ b/assets/health.rules.json @@ -9,7 +9,8 @@ "constants": { "UserTestMonitorInterval": null, "UndergraduateTestMonitorInterval": 4, - "DefaultTestMonitorInterval": 4 + "DefaultTestMonitorInterval": 4, + "FamilyMemberTestPrice": null }, "statuses": { @@ -69,7 +70,7 @@ } }, - "PCR.positive-NIP": { + "PCR.positive-NIP.0": { "condition": "timeout", "params": { "interval": { "min": 0, "max": 60, "scope": "future" } @@ -90,6 +91,80 @@ } }, + "PCR.positive-NIP": { + "condition": "test-interval", + "params": { + "interval": "UserTestMonitorInterval" + }, + "success": { + "condition": "require-test", + "params": { + "interval": { "min": 0, "max": "UserTestMonitorInterval", "scope": "future", "current": true } + }, + "success": { + "health_status": "yellow", + "priority": 1, + "next_step_html": "positive-nip.step.html", + "next_step_interval": "UserTestMonitorInterval", + "event_explanation": "positive-nip.explanation", + "warning": "test.future.warning" + }, + "fail": { + "health_status": "orange", + "priority": 1, + "next_step": "test.now.step", + "reason": "test.now.reason" + } + }, + "fail": { + "condition": "test-user", + "params": { + "card.role": "Undergraduate", + "card.student_level": "1U" + }, + "success": { + "condition": "require-test", + "params": { + "interval": { "min": 0, "max": "UndergraduateTestMonitorInterval", "scope": "future", "current": true } + }, + "success": { + "health_status": "yellow", + "priority": 1, + "next_step_html": "positive-nip.step.html", + "next_step_interval": "UndergraduateTestMonitorInterval", + "event_explanation": "positive-nip.explanation", + "warning": "test.future.warning" + }, + "fail": { + "health_status": "orange", + "priority": 1, + "next_step": "test.now.step", + "reason": "test.now.reason" + } + }, + "fail": { + "condition": "require-test", + "params": { + "interval": { "min": 0, "max": "DefaultTestMonitorInterval", "scope": "future", "current": true } + }, + "success": { + "health_status": "yellow", + "priority": 1, + "next_step_html": "positive-nip.step.html", + "next_step_interval": "DefaultTestMonitorInterval", + "event_explanation": "positive-nip.explanation", + "warning": "test.future.warning" + }, + "fail": { + "health_status": "orange", + "priority": 1, + "next_step": "test.now.step", + "reason": "test.now.reason" + } + } + } + }, + "PCR.negative": "test-monitor", "PCR.invalid": { @@ -733,16 +808,16 @@ "en": { "default.step": "Take a SHIELD Saliva Test when you return to campus.", "positive.step.html": "

You have been placed in mandatory isolation.

", - "positive-ip.step.html": "
Please continue to self-isolate, and you will be contacted by McKinley or Public Health in the next 24 hours. Please call the McKinley Dial-A-Nurse line if you have questions at 217-333-2700.


A small percentage of people may remain infectious following a 10-day isolation period. Usually, a brief 3-day period is all that is required before your test shows a safe level for you to gain access to all campus activities for the next 60 days. Your Safer Illinois App will remain in the DENIED STATUS until you are released through CUPHD and McKinley following your case review.
", - "positive-ip.explanation": "Your Saliva PCR test shows the VIRUS IS DETECTED in your REPEAT POSITIVE at a possibly INFECTIOUS LEVEL. You may require a longer isolation period.", - "positive-nip.step.html": "
You are not required to self-isolate, and you are not required to submit to campus testing for a period of 60 days unless directed to do so by your licensed health professional. Your Safer Illinois App is set to allow you access for this period. Contact covidwellness@illinois.edu if you have questions or need help.
", + "positive-ip.step.html": "

Please self-isolate and you will be contacted by Public Health in the next 24 hours. Please contact the University’s COVID Wellness Answer Center at covidwellness@illinois.edu or call 217-333-1900 if you have questions.

", + "positive-ip.explanation": "Your Saliva PCR test shows the VIRUS IS DETECTED. Due to the timing of this collection and the detection of virus, this is considered an INFECTIOUS POSITIVE. You require isolation to prevent viral spread to others.", + "positive-nip.step.html": "

You are not required to self-isolate unless you have COVID-like symptoms and are directed to do so by your licensed health professional.

", "positive-nip.explanation": "Your Saliva PCR test shows the VIRUS IS DETECTED in your REPEAT POSITIVE at a NON-INFECTIOUS LEVEL.", "test.monitor.step": "Monitor your test results", "test.now.step": "Get a test now", "test.now.reason": "Your status changed to Orange because you are past due for a test.", "test.another.asap.step": "Get another test asap", - "test.another.now.step.html": "

Get your second test now.

See testing schedule and rules.

", - "test.after.step.html": "

Get your second test after {next_step_date}. You must take two on-campus tests by Jan. 25.

See testing schedule and rules.

", + "test.another.now.step.html": "

Get your second test now.

See testing schedule and rules.

", + "test.after.step.html": "

Get your second test after {next_step_date}. You must take two on-campus tests by Jan. 25.

See testing schedule and rules.

", "test.after.reason": "Your status changed to Orange because you are required to take another test.", "test.required.reason": "Your status changed to Orange because you are required to get a test.", "test.resume.step": "Resume testing on your assigned days", @@ -780,16 +855,16 @@ "es": { "default.step": "Realice una prueba de saliva SHIELD cuando regrese al campus.", "positive.step.html": "

Se le ha puesto en aislamiento obligatorio.

", - "positive-ip.step.html": "
Continúe aislándose y McKinley o Public Health se comunicarán con usted en las próximas 24 horas. Llame a la línea McKinley Dial-A-Nurse si tiene preguntas al 217-333-2700.


Un pequeño porcentaje de personas puede seguir contagiado después de un período de aislamiento de 10 días. Por lo general, un breve período de 3 días es todo lo que se requiere antes de que su examen muestre un nivel seguro para que pueda acceder a todas las actividades del campus durante los próximos 60 días. Su aplicación Safer Illinois permanecerá en ESTADO DENEGADO hasta que CUPHD y McKinley lo den a conocer después de la revisión de su caso.
", - "positive-ip.explanation": "Su prueba de PCR de saliva muestra que el VIRUS ESTÁ DETECTADO en su REPETICIÓN POSITIVA a un NIVEL posiblemente INFECCIOSO. Es posible que necesite un período de aislamiento más prolongado.", - "positive-nip.step.html": "
No es necesario que se aísle por sí mismo ni que se someta a las pruebas del campus durante un período de 60 días, a menos que su profesional de la salud con licencia se lo indique. Su aplicación Safer Illinois está configurada para permitirle el acceso durante este período. Contacto covidwellness@illinois.edu si tiene preguntas o necesita ayuda.
", + "positive-ip.step.html": "

Por favor, aíslese y será contactado por Salud Pública en las próximas 24 horas. Comuníquese con el Centro de Respuestas de Bienestar COVID de la Universidad en covidwellness@illinois.edu o llame al 217-333-1900 si tiene preguntas.

", + "positive-ip.explanation": "Su prueba de PCR de saliva muestra que el VIRUS ESTÁ DETECTADO. Debido al tiempo de esta recolección y la detección de virus, esto se considera un POSITIVO INFECCIOSO. Necesita aislamiento para evitar la propagación viral a otros.", + "positive-nip.step.html": "

No es necesario que se aísle a sí mismo a menos que tenga síntomas similares a los de COVID y su profesional de la salud autorizado le indique que lo haga.

", "positive-nip.explanation": "Su prueba de PCR de saliva muestra que el VIRUS ESTÁ DETECTADO en su REPETICIÓN POSITIVA en un NIVEL NO INFECCIOSO.", "test.monitor.step": "Controle los resultados de su prueba", "test.now.step": "Haz una prueba ahora", "test.now.reason": "Su estado cambió a Naranja porque está atrasado en un examen.", "test.another.asap.step": "Obtenga otra prueba lo antes posible", - "test.another.now.step.html": "

Obtenga su segunda prueba ahora. Debes de tomar dos pruebas en el campus antes del 25 de enero.

Ver el calendario y las reglas de las pruebas.

", - "test.after.step.html": "

Obtenga su segunda prueba después del {next_step_date}. Debes de tomar dos pruebas en el campus antes del 25 de enero.

Ver el calendario y las reglas de las pruebas.

", + "test.another.now.step.html": "

Obtenga su segunda prueba ahora. Debes de tomar dos pruebas en el campus antes del 25 de enero.

Ver el calendario y las reglas de las pruebas.

", + "test.after.step.html": "

Obtenga su segunda prueba después del {next_step_date}. Debes de tomar dos pruebas en el campus antes del 25 de enero.

Ver el calendario y las reglas de las pruebas.

", "test.after.reason": "Su estado cambió a Naranja porque debe realizar otra prueba.", "test.required.reason": "Su estado cambió a Naranja porque debe realizar una prueba.", "test.resume.step": "Reanudar las pruebas en los días asignados", @@ -827,30 +902,30 @@ "zh": { "default.step": "返回校園後,請參加SHIELD唾液測試。", "positive.step.html": "

您已被強制隔離。

", - "positive-ip.step.html": "
請繼續進行自我隔離,在接下來的24小時內,麥金利或公共衛生將與您聯繫。 如果您有任何疑問,請致電McKinley Dial-A-Nurse熱線 217-333-2700.


在隔離期10天后,一小部分人可能仍然具有傳染性。 通常,只需要一個簡短的3天時間,測試就可以顯示一個安全的水平,讓您可以訪問接下來60天的所有校園活動。 在案件審查後,通過CUPHD和McKinley釋放您之前,您的“更安全的伊利諾伊州”應用程序將保持拒絕狀態。
", - "positive-ip.explanation": "您的唾液PCR測試顯示病毒在您的重複陽性中被檢測為可能具有感染水平。 您可能需要更長的隔離期。", - "positive-nip.step.html": "
您無需進行自我隔離,也不需要60天的時間進行校園測試,除非您的持牌醫護人員指示您這樣做。 您的伊利諾伊州安全應用程序已設置為允許您在此期間訪問。 如果您有疑問或需要幫助,請聯繫 covidwellness@illinois.edu
", + "positive-ip.step.html": "

請自我隔離,公共衛生將在接下來的24小時內與您聯繫。 如果您有任何疑問,請通過covidwellness@illinois.edu與大學的COVID健康解答中心聯繫,或致電217-333-1900

", + "positive-ip.explanation": "您的唾液PCR測試顯示病毒已被檢測到。 由於收集時間和病毒的檢測時間,這被認為是感染陽性。 您需要隔離以防止病毒傳播給他人。", + "positive-nip.step.html": "

除非您有類似COVID的症狀並且由您的有執照的衛生專業人員指示這樣做,否則您無需自我隔離。

", "positive-nip.explanation": "您的唾液PCR測試顯示病毒在非陽性水平的重複陽性中被檢測到。", "test.monitor.step": "監控您的測試結果", "test.now.step": "立即獲得測試", "test.now.reason": "您的狀態更改為“橙色”,因為您已逾期進行測試。", "test.another.asap.step": "盡快獲得另一個測試", - "test.another.now.step.html": "

现在进行第二次测试. 你必须在1月25日之前检测两次.

见测试计划和规则.

", - "test.after.step.html": "

在{next_step_date}之后进行第二次测试. 你必须在1月25日之前检测两次.

见测试计划和规则.

", + "test.another.now.step.html": "

现在进行第二次测试. 你必须在1月25日之前检测两次.

见测试计划和规则.

", + "test.after.step.html": "

在{next_step_date}之后进行第二次测试. 你必须在1月25日之前检测两次.

见测试计划和规则.

", "test.after.reason": "您的狀態更改為“橙色”,因為您需要參加另一項測試。", "test.required.reason": "您的狀態更改為橙色,因為需要進行測試。", "test.resume.step": "在您指定的日期恢復測試", "test.future.warning": "如果在{next_step_date}之前沒有任何負面測試,您將變成橙色/訪問被拒絕。", "test.past.reason": "您的狀態更改為“橙色”,因為您已逾期進行另一項測試。", - "test.multiple.step.html": "

你必须在1月25日之前检测两次.

见测试计划和规则.

", + "test.multiple.step.html": "

你必须在1月25日之前检测两次.

见测试计划和规则.

", "symptoms.step": "立即參加COVID-19測試", "symptoms.reason": "您的狀態更改為橙色,因為您自我報告的症狀與病毒一致。", "exposure.step.html": "

您可能已經接觸了感染了COVID-19的人。

", "exposure.reason": "您的狀態更改為橙色,因為您收到了曝光通知。", "quarantine-on.step": "呆在家裡,避免接觸", "quarantine-on.reason": "您的狀態更改為“橙色”,因為公共衛生部門已將您隔離。", - "release.step": "參加SHIELD唾液測試", - "release.reason": "您的狀態更改為“橙色”,因為公共衛生部門要求您進行測試。", + "release.step": "參加SHIELD唾液測試", + "release.reason": "您的狀態更改為“橙色”,因為公共衛生部門要求您進行測試。", "symptoms.no-symptoms": "無症狀", "symptoms.fever": "發熱", "symptoms.chills": "寒意", diff --git a/assets/strings.en.json b/assets/strings.en.json index 56f59933..596d5126 100644 --- a/assets/strings.en.json +++ b/assets/strings.en.json @@ -188,6 +188,7 @@ "panel.settings.home.user_info.title.sufix": "Welcome to Illinois", "panel.settings.home.account.title": "Your Account", "panel.settings.home.account.personal_info.title": "Personal Info", + "panel.settings.home.account.family_members.title": "Family Members", "panel.settings.home.account.options.payment": "Payment", "panel.settings.home.customizations.role.title": "Who you are", "panel.settings.home.customizations.manage_interests.title": "Manage your interests", @@ -262,6 +263,7 @@ "panel.profile_info.dialog.remove_my_information.subtitle": "Are you sure?", "panel.profile_info.dialog.remove_my_information.yes.title": "Yes", "panel.profile_info.dialog.remove_my_information.no.title": "No", + "panel.profile_info.dialog.remove_my_information.message.failed": "Failed to remove your information", "panel.covid19_passport.header.title": "COVID-19", "panel.covid19_passport.button.close.title": "Close", @@ -412,8 +414,8 @@ "panel.health.onboarding.covid19.disclosure.label.title": "Information Usage Disclosure", "panel.health.onboarding.covid19.disclosure.label.description1": "The Safer Illinois app uses:", "panel.health.onboarding.covid19.disclosure.label.content1": "1. Bluetooth to enable opt-in exposure notifications of close contact with individuals that test positive for COVID-19.", - "panel.health.onboarding.covid19.disclosure.label.content2": "2. Photos to allow a user to import their personal encryption key (QR code) into the app.", - "panel.health.onboarding.covid19.disclosure.label.content3": "3. Videos to allow a user to import their personal encryption key (QR code) into the app.", + "panel.health.onboarding.covid19.disclosure.label.content2": "2. Camera to allow a user to import their personal encryption key (QR code) into the app.", + "panel.health.onboarding.covid19.disclosure.label.content3": "3. Photos and Videos to allow a user to import their personal encryption key (QR code) into the app.", "panel.health.onboarding.covid19.disclosure.label.content4": "4. Files (external storage read and write) to allow a user to import their personal encryption key (QR code) into the app.", "panel.health.onboarding.covid19.disclosure.label.content5": "5. Location services on your device must be turned on to activate the Bluetooth low energy technology necessary for the exposure notification function of the Application to work in the background. However, the Application does not access, collect, or store any location data, including GPS data. If location services on your device are turned off, the Application will perform the limited functions of storing and providing information about COVID-19 test results, any voluntarily reported symptoms, and building access status.", "panel.health.onboarding.covid19.disclosure.label.description2": "YOUR INFORMATION AND HOW WE USE IT", @@ -692,6 +694,25 @@ "panel.health.symptoms.label.error.submit": "Failed to submit symptoms.", "panel.health.symptoms.label.error.no_selection": "Please select at least one symptom", + "panel.health.covid19.pending_family_member.label.text.statement1": "%s seeks your authorization to participate in Shield CU COVID-19 testing", + "panel.health.covid19.pending_family_member.label.text.statement2": "If you approve, your University account will be billed %s for each test taken.", + "panel.health.covid19.pending_family_member.label.text.terms.title": "Terms and Conditions", + "panel.health.covid19.pending_family_member.label.text.terms.html": "

The person named above has self-identified as a family or household member (“Family Member”) and has requested access to the COVID-19 testing services provided by the University of Illinois (“University”). By touching the “I Approve” button below, you agree that:

1. You are a University employee currently eligible for COVID-19 testing by the University;
2. Family Member is a family member in your household; and
3. You are responsible for any unpaid fees incurred by Family Member in obtaining COVID-19 testing services from the University.

You also agree that you are accepting responsibility on behalf of the Family Member if the Family Member is a minor under age 18 and you are the parent or legal guardian of the Family Member.

If you believe the person named above is improperly requesting access and/or has improperly obtained your University Identification Number (UIN), please contact shieldcu@illinois.edu.

", + "panel.health.covid19.pending_family_member.button.approve.title": "I Approve", + "panel.health.covid19.pending_family_member.button.disapprove.title": "I Disapprove", + "panel.health.covid19.pending_family_member.label.text.error": "Failed to submit", + + "panel.health.covid19.family_members.heading.title": "Family Members", + "panel.health.covid19.family_members.label.load_failed": "Failed to load family members", + "panel.health.covid19.family_members.label.submit_failed": "Failed to update status", + "panel.health.covid19.family_members.label.empty": "No family members", + "panel.health.covid19.family_members.label.status.accepted": "ACCEPTED", + "panel.health.covid19.family_members.label.status.revoked": "REVOKED", + "panel.health.covid19.family_members.label.status.pending": "PENDING", + "panel.health.covid19.family_members.label.status.unknown": "UNKNOWN", + "panel.health.covid19.family_members.button.accept.title": "Accept", + "panel.health.covid19.family_members.button.revoke.title": "Revoke", + "model.explore.time.today": "Today", "model.explore.time.tomorrow": "Tomorrow", "model.explore.time.at": "at", diff --git a/assets/strings.es.json b/assets/strings.es.json index 897b5a96..48418613 100644 --- a/assets/strings.es.json +++ b/assets/strings.es.json @@ -188,6 +188,7 @@ "panel.settings.home.user_info.title.sufix": "Bienvenido a Illinois", "panel.settings.home.account.title": "Su cuenta", "panel.settings.home.account.personal_info.title": "Información personal", + "panel.settings.home.account.family_members.title": "Miembros de la familia", "panel.settings.home.account.options.payment": "Pago", "panel.settings.home.customizations.role.title": "Quién eres", "panel.settings.home.customizations.manage_interests.title": "Administre sus intereses", @@ -262,6 +263,7 @@ "panel.profile_info.dialog.remove_my_information.subtitle": "¿Está seguro?", "panel.profile_info.dialog.remove_my_information.yes.title": "Sí", "panel.profile_info.dialog.remove_my_information.no.title": "No", + "panel.profile_info.dialog.remove_my_information.message.failed": "No se pudo eliminar su información", "panel.covid19_passport.header.title": "COVID-19", "panel.covid19_passport.button.close.title": "Cerrar", @@ -411,8 +413,8 @@ "panel.health.onboarding.covid19.disclosure.label.title": "Information Usage Disclosure", "panel.health.onboarding.covid19.disclosure.label.description1": "The Safer Illinois app uses:", "panel.health.onboarding.covid19.disclosure.label.content1": "1. Bluetooth to enable opt-in exposure notifications of close contact with individuals that test positive for COVID-19.", - "panel.health.onboarding.covid19.disclosure.label.content2": "2. Photos to allow a user to import their personal encryption key (QR code) into the app.", - "panel.health.onboarding.covid19.disclosure.label.content3": "3. Videos to allow a user to import their personal encryption key (QR code) into the app.", + "panel.health.onboarding.covid19.disclosure.label.content2": "2. Camera to allow a user to import their personal encryption key (QR code) into the app.", + "panel.health.onboarding.covid19.disclosure.label.content3": "3. Photos and Videos to allow a user to import their personal encryption key (QR code) into the app.", "panel.health.onboarding.covid19.disclosure.label.content4": "4. Files (external storage read and write) to allow a user to import their personal encryption key (QR code) into the app.", "panel.health.onboarding.covid19.disclosure.label.content5": "5. Location services on your device must be turned on to activate the Bluetooth low energy technology necessary for the exposure notification function of the Application to work in the background. However, the Application does not access, collect, or store any location data, including GPS data. If location services on your device are turned off, the Application will perform the limited functions of storing and providing information about COVID-19 test results, any voluntarily reported symptoms, and building access status.", "panel.health.onboarding.covid19.disclosure.label.description2": "YOUR INFORMATION AND HOW WE USE IT", @@ -691,6 +693,25 @@ "panel.health.symptoms.label.error.submit": "Error al enviar los síntomas.", "panel.health.symptoms.label.error.no_selection": "Please select at least one symptom", + "panel.health.covid19.pending_family_member.label.text.statement1": "%s seeks your authorization to participate in Shield CU COVID-19 testing", + "panel.health.covid19.pending_family_member.label.text.statement2": "If you approve, your University account will be billed %s for each test taken.", + "panel.health.covid19.pending_family_member.label.text.terms.title": "Terms and Conditions", + "panel.health.covid19.pending_family_member.label.text.terms.html": "

The person named above has self-identified as a family or household member (“Family Member”) and has requested access to the COVID-19 testing services provided by the University of Illinois (“University”). By touching the “I Approve” button below, you agree that:

1. You are a University employee currently eligible for COVID-19 testing by the University;
2. Family Member is a family member in your household; and
3. You are responsible for any unpaid fees incurred by Family Member in obtaining COVID-19 testing services from the University.

You also agree that you are accepting responsibility on behalf of the Family Member if the Family Member is a minor under age 18 and you are the parent or legal guardian of the Family Member.

If you believe the person named above is improperly requesting access and/or has improperly obtained your University Identification Number (UIN), please contact shieldcu@illinois.edu.

", + "panel.health.covid19.pending_family_member.button.approve.title": "I Approve", + "panel.health.covid19.pending_family_member.button.disapprove.title": "I Disapprove", + "panel.health.covid19.pending_family_member.label.text.error": "Failed to submit", + + "panel.health.covid19.family_members.heading.title": "Family Members", + "panel.health.covid19.family_members.label.load_failed": "Failed to load family members", + "panel.health.covid19.family_members.label.submit_failed": "Failed to update status", + "panel.health.covid19.family_members.label.empty": "No family members", + "panel.health.covid19.family_members.label.status.accepted": "ACCEPTED", + "panel.health.covid19.family_members.label.status.revoked": "REVOKED", + "panel.health.covid19.family_members.label.status.pending": "PENDING", + "panel.health.covid19.family_members.label.status.unknown": "UNKNOWN", + "panel.health.covid19.family_members.button.accept.title": "Accept", + "panel.health.covid19.family_members.button.revoke.title": "Revoke", + "model.explore.time.today": "Hoy", "model.explore.time.tomorrow": "Mañana", "model.explore.time.at": "a", diff --git a/assets/strings.zh.json b/assets/strings.zh.json index b80d9245..9be432ed 100644 --- a/assets/strings.zh.json +++ b/assets/strings.zh.json @@ -188,6 +188,7 @@ "panel.settings.home.user_info.title.sufix": "欢迎来到伊利诺伊州", "panel.settings.home.account.title": "您的帳戶", "panel.settings.home.account.personal_info.title": "个人信息", + "panel.settings.home.account.family_members.title": "家庭成員", "panel.settings.home.account.options.payment": "支付", "panel.settings.home.customizations.role.title": "你是", "panel.settings.home.customizations.manage_interests.title": "管理您的兴趣", @@ -262,6 +263,7 @@ "panel.profile_info.dialog.remove_my_information.subtitle": "确定吗?", "panel.profile_info.dialog.remove_my_information.yes.title": "是的", "panel.profile_info.dialog.remove_my_information.no.title": "不", + "panel.profile_info.dialog.remove_my_information.message.failed": "無法刪除您的信息", "panel.covid19_passport.header.title": "COVID-19", "panel.covid19_passport.button.close.title": "關", @@ -411,8 +413,8 @@ "panel.health.onboarding.covid19.disclosure.label.title": "Information Usage Disclosure", "panel.health.onboarding.covid19.disclosure.label.description1": "The Safer Illinois app uses:", "panel.health.onboarding.covid19.disclosure.label.content1": "1. Bluetooth to enable opt-in exposure notifications of close contact with individuals that test positive for COVID-19.", - "panel.health.onboarding.covid19.disclosure.label.content2": "2. Photos to allow a user to import their personal encryption key (QR code) into the app.", - "panel.health.onboarding.covid19.disclosure.label.content3": "3. Videos to allow a user to import their personal encryption key (QR code) into the app.", + "panel.health.onboarding.covid19.disclosure.label.content2": "2. Camera to allow a user to import their personal encryption key (QR code) into the app.", + "panel.health.onboarding.covid19.disclosure.label.content3": "3. Photos and Videos to allow a user to import their personal encryption key (QR code) into the app.", "panel.health.onboarding.covid19.disclosure.label.content4": "4. Files (external storage read and write) to allow a user to import their personal encryption key (QR code) into the app.", "panel.health.onboarding.covid19.disclosure.label.content5": "5. Location services on your device must be turned on to activate the Bluetooth low energy technology necessary for the exposure notification function of the Application to work in the background. However, the Application does not access, collect, or store any location data, including GPS data. If location services on your device are turned off, the Application will perform the limited functions of storing and providing information about COVID-19 test results, any voluntarily reported symptoms, and building access status.", "panel.health.onboarding.covid19.disclosure.label.description2": "YOUR INFORMATION AND HOW WE USE IT", @@ -691,6 +693,25 @@ "panel.health.symptoms.label.error.submit": "未能提交症状.", "panel.health.symptoms.label.error.no_selection": "Please select at least one symptom", + "panel.health.covid19.pending_family_member.label.text.statement1": "%s seeks your authorization to participate in Shield CU COVID-19 testing", + "panel.health.covid19.pending_family_member.label.text.statement2": "If you approve, your University account will be billed %s for each test taken.", + "panel.health.covid19.pending_family_member.label.text.terms.title": "Terms and Conditions", + "panel.health.covid19.pending_family_member.label.text.terms.html": "

The person named above has self-identified as a family or household member (“Family Member”) and has requested access to the COVID-19 testing services provided by the University of Illinois (“University”). By touching the “I Approve” button below, you agree that:

1. You are a University employee currently eligible for COVID-19 testing by the University;
2. Family Member is a family member in your household; and
3. You are responsible for any unpaid fees incurred by Family Member in obtaining COVID-19 testing services from the University.

You also agree that you are accepting responsibility on behalf of the Family Member if the Family Member is a minor under age 18 and you are the parent or legal guardian of the Family Member.

If you believe the person named above is improperly requesting access and/or has improperly obtained your University Identification Number (UIN), please contact shieldcu@illinois.edu.

", + "panel.health.covid19.pending_family_member.button.approve.title": "I Approve", + "panel.health.covid19.pending_family_member.button.disapprove.title": "I Disapprove", + "panel.health.covid19.pending_family_member.label.text.error": "Failed to submit", + + "panel.health.covid19.family_members.heading.title": "Family Members", + "panel.health.covid19.family_members.label.load_failed": "Failed to load family members", + "panel.health.covid19.family_members.label.submit_failed": "Failed to update status", + "panel.health.covid19.family_members.label.empty": "No family members", + "panel.health.covid19.family_members.label.status.accepted": "ACCEPTED", + "panel.health.covid19.family_members.label.status.revoked": "REVOKED", + "panel.health.covid19.family_members.label.status.pending": "PENDING", + "panel.health.covid19.family_members.label.status.unknown": "UNKNOWN", + "panel.health.covid19.family_members.button.accept.title": "Accept", + "panel.health.covid19.family_members.button.revoke.title": "Revoke", + "model.explore.time.today": "今天", "model.explore.time.tomorrow": "明天", "model.explore.time.at": "在", diff --git a/lib/model/Health.dart b/lib/model/Health.dart index 9ed63291..cfafab47 100644 --- a/lib/model/Health.dart +++ b/lib/model/Health.dart @@ -1684,6 +1684,134 @@ class HealthGuidelineItem { } } +/////////////////////////////// +// HealthFamilyMember + +class HealthFamilyMember { + String id; + DateTime dateCreated; + String groupName; + String status; + String applicantFirstName; + String applicantLastName; + String applicantEmail; + String applicantPhone; + String approverId; + String approverLastName; + + static const String StatusAccepted = 'accepted'; + static const String StatusRevoked = 'rejected'; + static const String StatusPending = 'pending'; + + HealthFamilyMember({this.id, this.dateCreated, this.groupName, this.status, + this.applicantFirstName, this.applicantLastName, this.applicantEmail, this.applicantPhone, + this.approverId, this.approverLastName}); + + factory HealthFamilyMember.fromJson(Map json){ + return (json != null) ? HealthFamilyMember( + id: json['id'], + dateCreated: healthDateTimeFromString(json['date_created']), + groupName: json['group_name'], + status: json['status'], + applicantFirstName: json['first_name'], + applicantLastName: json['last_name'], + applicantEmail: json['email'], + applicantPhone: json['phone'], + approverId: json['external_approver_id'], + approverLastName: json['external_approver_last_name'], + ) : null; + } + + Map toJson() { + return { + 'id': id, + 'date_created': healthDateTimeToString(dateCreated), + 'group_name': groupName, + 'status': status, + 'first_name': applicantFirstName, + 'last_name': applicantLastName, + 'email': applicantEmail, + 'phone': applicantPhone, + 'external_approver_id': approverId, + 'external_approver_last_name': approverLastName, + }; + } + + String get applicantFullName { + if (AppString.isStringNotEmpty(applicantFirstName)) { + if (AppString.isStringNotEmpty(applicantLastName)) { + return "$applicantFirstName $applicantLastName"; + } + else { + return "$applicantFirstName"; + } + } + else { + return "$applicantLastName"; + } + } + + String get applicantEmailOrPhone { + if (AppString.isStringNotEmpty(applicantEmail)) { + return applicantEmail; + } + else if (AppString.isStringNotEmpty(applicantPhone)) { + return applicantPhone; + } + else { + return null; + } + } + + bool get isPending { + return status == StatusPending; + } + + bool get isAcepted { + return status == StatusAccepted; + } + + bool get isRevoked { + return status == StatusRevoked; + } + + static List listFromJson(List json) { + List values; + if (json != null) { + values = []; + for (dynamic entry in json) { + HealthFamilyMember value; + try { value = HealthFamilyMember.fromJson((entry as Map)?.cast()); } + catch(e) { print(e.toString()); } + values.add(value); + } + } + return values; + } + + static List listToJson(List values) { + List json; + if (values != null) { + json = []; + for (HealthFamilyMember value in values) { + json.add(value?.toJson()); + } + } + return json; + } + + static HealthFamilyMember pendingMemberFromList(List values) { + if (values != null) { + for (HealthFamilyMember member in values) { + if (member.isPending) { + return member; + } + } + } + return null; + } +} + /////////////////////////////// // HealthSymptom @@ -1839,6 +1967,7 @@ class HealthRulesSet { final Map strings; static const String UserTestMonitorInterval = 'UserTestMonitorInterval'; + static const String FamilyMemberTestPrice = 'FamilyMemberTestPrice'; HealthRulesSet({this.tests, this.symptoms, this.contactTrace, this.actions, this.defaults, this.statuses, Map constants, Map strings}) : this.constants = constants ?? Map(), @@ -1865,6 +1994,10 @@ class HealthRulesSet { constants[UserTestMonitorInterval] = value; } + String get familyMemberTestPrice { + return localeString(constants[FamilyMemberTestPrice]); + } + String localeString(dynamic entry) { if ((strings != null) && (entry is String)) { String currentLanguage = Localization().currentLocale?.languageCode; diff --git a/lib/service/Auth.dart b/lib/service/Auth.dart index 410d4d54..fe9fb523 100644 --- a/lib/service/Auth.dart +++ b/lib/service/Auth.dart @@ -611,7 +611,7 @@ class Auth with Service implements NotificationsListener { try { Response response = await Network().delete(url, headers: {'Content-Type': 'application/json'}, auth: NetworkAuth.User); - if(response?.statusCode == 200) { + if((response != null) && (200 <= response.statusCode) && (response.statusCode <= 205)) { // returns 202 Accepted _applyUserPiiData(null, null); return true; } diff --git a/lib/service/Health.dart b/lib/service/Health.dart index fa1847b5..35b79c05 100644 --- a/lib/service/Health.dart +++ b/lib/service/Health.dart @@ -50,6 +50,8 @@ class Health with Service implements NotificationsListener { static const String notifyHistoryUpdated = "edu.illinois.rokwire.health.history.updated"; static const String notifyUserUpdated = "edu.illinois.rokwire.health.user.updated"; static const String notifyUserPrivateKeyUpdated = "edu.illinois.rokwire.health.user.private_key.updated"; + static const String notifyFamilyMembersAvailable = "edu.illinois.rokwire.health.family.members.available"; + static const String notifyPendingFamilyMember = "edu.illinois.rokwire.health.family.member.pending"; static const String notifyProcessingFinished = "edu.illinois.rokwire.health.processing.finished"; @@ -113,7 +115,9 @@ class Health with Service implements NotificationsListener { @override void initServiceUI() { - _process(); + _process().then((_) { + checkPendingFamilyMembers(); + }); } @override @@ -163,6 +167,7 @@ class Health with Service implements NotificationsListener { if (Config().refreshTimeout < pausedDuration.inSeconds) { _refreshUser().then((_) { _process(); + checkPendingFamilyMembers(); }); } } @@ -1625,6 +1630,54 @@ class Health with Service implements NotificationsListener { } return false; } + + + // Membership application + + Future> loadFamilyMembers() async { + String url = (Config().healthUrl != null) ? "${Config().healthUrl}/covid19/join-external-approvements" : null; + Response response = (url != null) ? await Network().get(url, auth: NetworkAuth.User) : null; + String responseBody = (response?.statusCode == 200) ? response.body : null; + List responseJson = (responseBody != null) ? AppJson.decodeList(responseBody) : null; + /*responseJson = [ + { "id": "1234", "first_name": "Petyo", "last_name": "Stoyanov", "date_created": "2021-02-19T10:27:43.679Z", "group_name": "U of Illinois employee family member", "external_approver_id": "68572", "external_approver_last_name": "Varbanov", "status":"pending" }, + { "id": "5678", "first_name": "Mladen", "last_name": "Dryankov", "date_created": "2021-02-19T10:27:43.679Z", "group_name": "U of Illinois employee family member", "external_approver_id": "68572", "external_approver_last_name": "Varbanov", "status":"accepted" }, + { "id": "9101", "first_name": "Todor", "last_name": "Bachvarov", "date_created": "2021-02-19T10:27:43.679Z", "group_name": "U of Illinois employee family member", "external_approver_id": "68572", "external_approver_last_name": "Varbanov", "status":"rejected" }, + ];*/ + + return (responseJson != null) ? HealthFamilyMember.listFromJson(responseJson) : null; + } + + Future checkPendingFamilyMembers() async { + if (Storage().onBoardingPassed && _isLoggedIn) { + List members = await loadFamilyMembers(); + if (members != null) { + NotificationService().notify(notifyFamilyMembersAvailable, members); + + HealthFamilyMember pendingMember = HealthFamilyMember.pendingMemberFromList(members); + if (pendingMember != null) { + NotificationService().notify(notifyPendingFamilyMember, pendingMember); + } + } + } + } + + // Return: + // - true, on succeess + // - false or error string on fail + Future applyFamilyMemberStatus(HealthFamilyMember member, String status) async { + String url = (Config().healthUrl != null) ? "${Config().healthUrl}/covid19/join-external-approvements/${member?.id}" : null; + String post = AppJson.encode({'status': status }); + Response response = (url != null) ? await Network().put(url, body: post, auth: NetworkAuth.User) : null; + + if (response?.statusCode == 200) { + return true; + } + else { + String responseBody = response?.body; + return (AppString.isStringNotEmpty(responseBody)) ? responseBody : false; + } + } } class _ProcessResult { diff --git a/lib/ui/RootPanel.dart b/lib/ui/RootPanel.dart index 363c53c6..4f4e3c8d 100644 --- a/lib/ui/RootPanel.dart +++ b/lib/ui/RootPanel.dart @@ -26,6 +26,7 @@ import 'package:illinois/service/Service.dart'; import 'package:illinois/service/Analytics.dart'; import 'package:illinois/service/Localization.dart'; import 'package:illinois/service/NotificationService.dart'; +import 'package:illinois/ui/settings/SettingsPendingFamilyMemberPanel.dart'; import 'package:illinois/ui/health/Covid19HistoryPanel.dart'; import 'package:illinois/ui/health/Covid19InfoCenterPanel.dart'; import 'package:illinois/ui/health/Covid19StatusPanel.dart'; @@ -50,6 +51,8 @@ class _RootPanelState extends State with SingleTickerProviderStateMix static const String HEALTH_STATUS_URI = 'edu.illinois.covid://covid.illinois.edu/health/status'; + bool _presentingMemberApplication; + @override void initState() { super.initState(); @@ -60,6 +63,7 @@ class _RootPanelState extends State with SingleTickerProviderStateMix Organizations.notifyOrganizationChanged, Organizations.notifyEnvironmentChanged, Health.notifyStatusUpdated, + Health.notifyPendingFamilyMember, DeepLink.notifyUri, ]); @@ -91,6 +95,9 @@ class _RootPanelState extends State with SingleTickerProviderStateMix else if (name == Health.notifyStatusUpdated) { _presentHealthStatusUpdate(param); } + else if (name == Health.notifyPendingFamilyMember) { + _presentPedningFamilyMember(param); + } else if (name == FirebaseMessaging.notifyCovid19Notification) { _onFirebaseCovid19Notification(param); } @@ -224,6 +231,18 @@ class _RootPanelState extends State with SingleTickerProviderStateMix Navigator.push(context, CupertinoPageRoute(builder: (context) => Covid19StatusUpdatePanel(status: params['status'], previousHealthStatus: params['lastHealthStatus'],))); } + void _presentPedningFamilyMember(dynamic param) { + if (_presentingMemberApplication != true) { + _presentingMemberApplication = true; + Navigator.push(context, PageRouteBuilder( opaque: false, pageBuilder: (context, _, __) => SettingsPendingFamilyMemberPanel(pendingMember: param))).then((result) { + _presentingMemberApplication = false; + if (result == true) { + Health().checkPendingFamilyMembers(); + } + }); + } + } + void _onDeeplinkUri(Uri uri) { if (uri != null) { Uri healthStatusUri; diff --git a/lib/ui/health/Covid19InfoCenterPanel.dart b/lib/ui/health/Covid19InfoCenterPanel.dart index 27f684be..ec5c99c0 100644 --- a/lib/ui/health/Covid19InfoCenterPanel.dart +++ b/lib/ui/health/Covid19InfoCenterPanel.dart @@ -60,7 +60,7 @@ class Covid19InfoCenterPanel extends StatefulWidget { class _Covid19InfoCenterPanelState extends State implements NotificationsListener { Covid19Status _status; - bool _covid19Access = false; + bool _covid19Access; bool _loadingStatus; Covid19History _lastHistory; String _currentCountyName; @@ -195,14 +195,22 @@ class _Covid19InfoCenterPanelState extends State impleme }); } + Future _onPullToRefresh() async { + print("Covid19InfoCenterPanel pullToRefresh"); + _loadStatus(); + Health().checkPendingFamilyMembers(); + } + @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Styles().colors.background, appBar: _Covid19HomeHeaderBar(context: context,), - body: SingleChildScrollView( - child: SafeArea( - child: _buildMainContent(), + body: RefreshIndicator(onRefresh: _onPullToRefresh, + child: SingleChildScrollView( + child: SafeArea( + child: _buildMainContent(), + ), ), ), ); @@ -811,8 +819,9 @@ class _Covid19InfoCenterPanelState extends State impleme } String get _accessStatusText{ - return (_covid19Access? Localization().getStringEx("panel.covid19home.label.access.granted","Building access granted"): - Localization().getStringEx("panel.covid19home.label.access.denied","Building access denied")); + return (_covid19Access == true ? + Localization().getStringEx("panel.covid19home.label.access.granted", "Building access granted"): + Localization().getStringEx("panel.covid19home.label.access.denied", "Building access denied")); } } diff --git a/lib/ui/onboarding/OnboardingHealthDisclosurePanel.dart b/lib/ui/onboarding/OnboardingHealthDisclosurePanel.dart index abe2a486..ff7232c4 100644 --- a/lib/ui/onboarding/OnboardingHealthDisclosurePanel.dart +++ b/lib/ui/onboarding/OnboardingHealthDisclosurePanel.dart @@ -134,12 +134,12 @@ class _OnBoardingHealthDisclosurePanelState extends State _SettingsFamilyMembersPanelState(); +} + +class _SettingsFamilyMembersPanelState extends State implements NotificationsListener { + + bool _loading; + List _members; + + @override + void initState() { + super.initState(); + NotificationService().subscribe(this, [ + Health.notifyFamilyMembersAvailable, + ]); + _loadMembers(); + } + + @override + void dispose() { + NotificationService().unsubscribe(this); + super.dispose(); + } + + // NotificationsListener + + @override + void onNotification(String name, dynamic param) { + if (name == Health.notifyFamilyMembersAvailable) { + if (mounted && (param != null)) { + setState(() { + _members = param; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold(backgroundColor: Styles().colors.background, + appBar: SimpleHeaderBarWithBack(context: context, + titleWidget: Text(Localization().getStringEx('panel.health.covid19.family_members.heading.title', 'Family Members'), + style: TextStyle(color: Colors.white, fontSize: 16, fontFamily: Styles().fontFamilies.extraBold),),), + body: SafeArea(child: _buildContent()) + ); + } + + Widget _buildContent() { + if (_loading == true) { + return _buildLoading(); + } + else if (_members == null) { + return _buildStatusText(Localization().getStringEx('panel.health.covid19.family_members.label.load_failed', 'Failed to load family members')); + } + else if (_members.length == 0) { + return _buildStatusText(Localization().getStringEx('panel.health.covid19.family_members.label.empty', 'No family members')); + } + else { + return _buildMembers(); + } + } + + void _loadMembers() { + setState(() { + _loading = true; + }); + Health().loadFamilyMembers().then((List result) { + if (mounted) { + setState(() { + _loading = false; + _members = result; + }); + } + }); + } + + Widget _buildLoading() { + return Align(alignment: Alignment.center, child: + CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorSecondary), strokeWidth: 3,), + ); + } + + Widget _buildStatusText(String statusText) { + return Padding(padding: EdgeInsets.all(30), child: + Column(children: [ + Expanded(flex: 1, child: Container()), + Text(statusText, textAlign: TextAlign.center, style: TextStyle(color: Styles().colors.fillColorPrimary, fontSize: 20, fontFamily: Styles().fontFamilies.bold),), + Expanded(flex: 2, child: Container()), + ],) + ); + } + + Widget _buildMembers() { + List content = []; + for (HealthFamilyMember member in _members) { + content.add(Padding(padding: EdgeInsets.only(top: content.isNotEmpty ? 8 : 0), child: _FamilyMemberWidget(member: member),)); + } + return Padding(padding: EdgeInsets.symmetric(horizontal: 16, vertical: 32), child: + Column(children: content), + ); + } + +} + +class _FamilyMemberWidget extends StatefulWidget { + final HealthFamilyMember member; + _FamilyMemberWidget({this.member}); + + @override + _FamilyMemberWidgetState createState() => _FamilyMemberWidgetState(); +} + +class _FamilyMemberWidgetState extends State<_FamilyMemberWidget> { + + bool _buttonsEnabled = true; + bool _hasProgress = false; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + String status; + List buttons = []; + if (widget.member.isAcepted) { + status = Localization().getStringEx('panel.health.covid19.family_members.label.status.accepted', 'ACCEPTED'); + buttons.add(_buildCommandButton(Localization().getStringEx('panel.health.covid19.family_members.button.revoke.title', 'Revoke'), _onRevoke)); + } + else if (widget.member.isRevoked) { + status = Localization().getStringEx('panel.health.covid19.family_members.label.status.revoked', 'REVOKED'); + buttons.add(_buildCommandButton(Localization().getStringEx('panel.health.covid19.family_members.button.accept.title', 'Accept'), _onAccept)); + } + else if (widget.member.isPending) { + status = Localization().getStringEx('panel.health.covid19.family_members.label.status.pending', 'PENDING'); + buttons.addAll([ + _buildCommandButton(Localization().getStringEx('panel.health.covid19.family_members.button.accept.title', 'Accept'), _onAccept), + Container(width: 8,), + _buildCommandButton(Localization().getStringEx('panel.health.covid19.family_members.button.revoke.title', 'Revoke'), _onRevoke), + ]); + } + else { + status = widget.member.status?.toUpperCase() ?? Localization().getStringEx('panel.health.covid19.family_members.label.status.unknown', 'UNKNOWN'); + } + + buttons.addAll([ + Expanded(child: Container()), + _buildProgress(), + ]); + + + return Container( + decoration: BoxDecoration( + color: Styles().colors.white, + borderRadius: BorderRadius.all(Radius.circular(4)), + boxShadow: [BoxShadow(color: Styles().colors.blackTransparent018, spreadRadius: 2.0, blurRadius: 6.0, offset: Offset(2, 2))],), + child: Stack(children: [ + Visibility(visible: (status != null), child: + Align(alignment: Alignment.topRight, child: + Padding(padding: EdgeInsets.only(top: 8, right: 8), child: + Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration(color: Styles().colors.fillColorPrimary, borderRadius: BorderRadius.all(Radius.circular(2)),), + child: Text(status ?? '', style: TextStyle(color: Styles().colors.textColorPrimary, fontSize: 12, fontFamily: Styles().fontFamilies.bold),), + ), + ), + ), + ), + Padding(padding: EdgeInsets.all(12), child: + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(widget.member.applicantFullName ?? '', style: TextStyle(color: Styles().colors.fillColorPrimary, fontSize: 20, fontFamily: Styles().fontFamilies.bold),), + Text(widget.member.applicantEmailOrPhone ?? '', style: TextStyle(color: Styles().colors.fillColorPrimaryVariant, fontSize: 16, fontFamily: Styles().fontFamilies.medium),), + Padding(padding: EdgeInsets.only(top: 8), child: + Row(mainAxisAlignment: MainAxisAlignment.start, children: buttons), + ), + ],), + ), + ],) + ); + } + + Widget _buildCommandButton(String title, handler) { + return Stack(children: [ + RoundedButton( + label: title, + hint: '', + fontSize: 18, + height: 42, + padding: EdgeInsets.symmetric(horizontal: 12), + textColor: _buttonsEnabled ? Styles().colors.fillColorPrimary : Styles().colors.surfaceAccent, + borderColor: _buttonsEnabled ? Styles().colors.fillColorPrimary : Styles().colors.surfaceAccent, + backgroundColor: Styles().colors.surface, + onTap: handler, + ), + ]); + } + + Widget _buildProgress() { + return Visibility(visible: (_hasProgress == true), child: + Container(width: 42, height: 42, child: + Center(child: + Container(width: 21, height: 21, child: + CircularProgressIndicator(strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorSecondary)), + ), + ), + ), + ); + } + + + void _onAccept() { + _submit(HealthFamilyMember.StatusAccepted); + } + + void _onRevoke() { + _submit(HealthFamilyMember.StatusRevoked); + } + + void _submit(String status) { + setState(() { + _hasProgress = true; + _buttonsEnabled = false; + }); + Health().applyFamilyMemberStatus(widget.member, status).then((dynamic result) { + if (mounted) { + if (result == true) { + setState(() { + _hasProgress = false; + _buttonsEnabled = true; + widget.member.status = status; + }); + } + else { + setState(() { + _hasProgress = false; + }); + String errorMessage = (result is String) ? result : Localization().getStringEx('panel.health.covid19.family_members.label.submit_failed', 'Failed to update status'); + AppAlert.showDialogResult(context, errorMessage).then((_) { + setState(() { + _buttonsEnabled = true; + }); + }); + } + } + }); + } +} \ No newline at end of file diff --git a/lib/ui/settings/SettingsHomePanel.dart b/lib/ui/settings/SettingsHomePanel.dart index 27103b23..294b6c57 100644 --- a/lib/ui/settings/SettingsHomePanel.dart +++ b/lib/ui/settings/SettingsHomePanel.dart @@ -27,6 +27,7 @@ import 'package:illinois/service/Auth.dart'; import 'package:illinois/service/Connectivity.dart'; import 'package:illinois/service/Organizations.dart'; import 'package:illinois/ui/onboarding/OnboardingLoginPhoneVerifyPanel.dart'; +import 'package:illinois/ui/settings/SettingsFamilyMembersPanel.dart'; import 'package:illinois/ui/widgets/HeaderBar.dart'; import 'package:illinois/utils/AppDateTime.dart'; import 'package:illinois/service/FirebaseMessaging.dart'; @@ -1042,12 +1043,20 @@ class _SettingsHomePanelState extends State implements Notifi String code = codes[index]; BorderRadius borderRadius = _borderRadiusFromIndex(index, codes.length); if (code == 'personal_info') { - contentList.add(RibbonButton( + contentList.add(Padding(padding: EdgeInsets.only(top: contentList.isNotEmpty ? 8 : 0), child: RibbonButton( height: null, border: Border.all(color: Styles().colors.surfaceAccent, width: 0), borderRadius: borderRadius, label: Localization().getStringEx("panel.settings.home.account.personal_info.title", "Personal Info"), - onTap: _onPersonalInfoClicked)); + onTap: _onPersonalInfoClicked))); + } + else if (code == 'family_members') { + contentList.add(Padding(padding: EdgeInsets.only(top: contentList.isNotEmpty ? 8 : 0), child: RibbonButton( + height: null, + border: Border.all(color: Styles().colors.surfaceAccent, width: 0), + borderRadius: borderRadius, + label: Localization().getStringEx("panel.settings.home.account.family_members.title", "Family Members"), + onTap: _onFamilyMembersClicked))); } } @@ -1068,6 +1077,17 @@ class _SettingsHomePanelState extends State implements Notifi } } + void _onFamilyMembersClicked() { + if (Connectivity().isNotOffline) { + Analytics.instance.logSelect(target: "Family Members"); + if (Auth().isLoggedIn) { + Navigator.push(context, CupertinoPageRoute(builder: (context) => SettingsFamilyMembersPanel())); + } + } else { + AppAlert.showOfflineMessage(context); + } + } + // Feedback Widget _buildFeedback(){ diff --git a/lib/ui/settings/SettingsPendingFamilyMemberPanel.dart b/lib/ui/settings/SettingsPendingFamilyMemberPanel.dart new file mode 100644 index 00000000..c8d44b4a --- /dev/null +++ b/lib/ui/settings/SettingsPendingFamilyMemberPanel.dart @@ -0,0 +1,233 @@ + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_html/style.dart'; +import 'package:illinois/model/Health.dart'; +import 'package:illinois/service/Analytics.dart'; +import 'package:illinois/service/Health.dart'; +import 'package:illinois/service/Localization.dart'; +import 'package:illinois/service/Styles.dart'; +import 'package:illinois/ui/widgets/RoundedButton.dart'; +import 'package:sprintf/sprintf.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class SettingsPendingFamilyMemberPanel extends StatefulWidget { + final HealthFamilyMember pendingMember; + + SettingsPendingFamilyMemberPanel({this.pendingMember}); + + @override + _SettingsPendingFamilyMemberPanelState createState() => _SettingsPendingFamilyMemberPanelState(); +} + +class _SettingsPendingFamilyMemberPanelState extends State { + + bool _hasProgress = false; + bool _buttonsEnabled = true; + bool _termsAccepted = false; + String _errorMessage; + HealthRulesSet _rules; + + @override + void initState() { + Health().loadRules2().then((HealthRulesSet rules) { + if (mounted) { + setState(() { + _rules = rules; + }); + } + }); + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + double screenWidth = MediaQuery.of(context).size.width; + return Scaffold(backgroundColor: Styles().colors.fillColorPrimary.withOpacity(0.3), body: + SafeArea(child: + Padding(padding: EdgeInsets.only(left: screenWidth / 12, right: screenWidth / 12, top: kToolbarHeight), child: + Column(children: [ + Expanded(flex: 1, child: Container()), + Container(decoration: BoxDecoration(color: Styles().colors.fillColorPrimary, border:Border.all(color: Styles().colors.fillColorSecondary, width: 1)), child: + Stack(children: [ + _buildContent(), + Container(alignment: Alignment.topRight, child: _buildCloseButton()), + Container(alignment: Alignment.center, padding: EdgeInsets.only(top: 96), child: _buildProgressIndicator()), + ]), + ), + Expanded(flex: 2, child: Container()), + ],), + ))); + } + + Widget _buildContent() { + String statement1Text = sprintf(Localization().getStringEx('panel.health.covid19.pending_family_member.label.text.statement1', '%s seeks your authorization to participate in Shield CU COVID-19 testing.'), [widget.pendingMember.applicantFullName]); + String statement2Text = sprintf(Localization().getStringEx('panel.health.covid19.pending_family_member.label.text.statement2', 'If you approve, your University account will be billed %s for each test taken.'), [_rules?.familyMemberTestPrice ?? '\$xx']); + String termsText = Localization().getStringEx('panel.health.covid19.pending_family_member.label.text.terms.title', 'Terms and Conditions'); + String checkImageText = _termsAccepted ? '\u2611' : '\u2610'; + + Color termsColor = Styles().colors.fillColorSecondary, errorColor = Colors.yellow; + TextStyle textStyle = TextStyle(color: Styles().colors.textColorPrimary, fontFamily: Styles().fontFamilies.bold, fontSize: 18); + TextStyle termsTextStyle = TextStyle(color: termsColor, fontFamily: Styles().fontFamilies.bold, fontSize: 18, decoration: TextDecoration.underline); + TextStyle termsImageStyle = TextStyle(color: termsColor, fontFamily: Styles().fontFamilies.bold, fontSize: 28); + TextStyle errorTextStyle = TextStyle(color: errorColor, fontFamily: Styles().fontFamilies.regular, fontSize: 16); + + List statements = [ + Text(statement1Text, style: textStyle, ), + Container(height: 18), + Text(statement2Text, style: textStyle, ), + InkWell(onTap: _onTerms, child: Column(children:[ + Container(height: 9), + Row(children:[ + Text(checkImageText, style: termsImageStyle), + Container(width: 9,), + Text(termsText, style: termsTextStyle), + ] ), + Container(height: 9), + ],),), + ]; + + if (_errorMessage != null) { + statements.addAll([ + Text(_errorMessage, style: errorTextStyle, ), + Container(height: 9), + ]); + } + + return Padding(padding: EdgeInsets.all(20), child: + Column(crossAxisAlignment: CrossAxisAlignment.start, children:[ + Padding(padding: EdgeInsets.only(right: 10), child: + Column(crossAxisAlignment: CrossAxisAlignment.start, children: + statements + ), + ), + Align(alignment: Alignment.center, child: + Wrap(runSpacing: 8, spacing: 12, children: [ + _buildCommandButton(Localization().getStringEx('panel.health.covid19.pending_family_member.button.approve.title', 'I Approve'), _onApprove), + _buildCommandButton(Localization().getStringEx('panel.health.covid19.pending_family_member.button.disapprove.title', 'I Disapprove'), _onDisapprove), + ]), + ), + ])); + } + + Widget _buildCloseButton() { + return Semantics(label: Localization().getStringEx('dialog.close.title', 'Close'), button: true, excludeSemantics: true, child: + InkWell(onTap : _onClose, child: + Container(width: 48, height: 48, alignment: Alignment.center, child: + Image.asset('images/close-white.png') + ))); + } + + Widget _buildCommandButton(String title, handler) { + return Row(mainAxisSize: MainAxisSize.min, children: [ + RoundedButton( + label: title, + hint: '', + fontSize: 18, + padding: EdgeInsets.symmetric(horizontal: 12), + textColor: _buttonsEnabled ? Styles().colors.fillColorPrimary : Styles().colors.surfaceAccent, + borderColor: _buttonsEnabled ? Styles().colors.fillColorSecondary : Styles().colors.surfaceAccent, + backgroundColor: Styles().colors.surface, + onTap: handler, + ), + ]); + } + + Widget _buildProgressIndicator() { + return Visibility(visible: _hasProgress, child: + CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorSecondary), strokeWidth: 3,)); + } + + Widget _buildTermsDialog(BuildContext context) { + String termsHtml = Localization().getStringEx('panel.health.covid19.pending_family_member.label.text.terms.html', """ +

The person named above has self-identified as a family or household member (“Family Member”) and has requested access to the COVID-19 testing services provided by the University of Illinois (“University”). By touching the “I Approve” button below, you agree that:

+

+1. You are a University employee currently eligible for COVID-19 testing by the University;
+2. Family Member is a family member in your household; and
+3. You are responsible for any unpaid fees incurred by Family Member in obtaining COVID-19 testing services from the University. +

+

You also agree that you are accepting responsibility on behalf of the Family Member if the Family Member is a minor under age 18 and you are the parent or legal guardian of the Family Member.

+

If you believe the person named above is improperly requesting access and/or has improperly obtained your University Identification Number (UIN), please contact shieldcu@illinois.edu.

"""); + Style htmlStyle = Style(color: Styles().colors.fillColorPrimary, fontFamily: Styles().fontFamilies.bold, fontSize: FontSize(16)); + return ClipRRect(borderRadius: BorderRadius.all(Radius.circular(8)), child: + Dialog(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8),), child: + Stack(children: [ + Padding(padding: EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: + SingleChildScrollView(child: + Html(data: termsHtml, style: { 'body': htmlStyle }, onLinkTap: (url) => launch(url)), + ), + ), + Container(alignment: Alignment.topRight, height: 42, child: InkWell(onTap: _onCloseTerms, child: Container(width: 42, height: 42, alignment: Alignment.center, child: Image.asset('images/close-blue.png')))), + ],), + ), + ); + + } + + void _onTerms() { + Analytics.instance.logSelect(target: "Terms"); + if (!_termsAccepted) { + showDialog(context: context, builder: (context) => _buildTermsDialog(context) ).then((_) { + setState(() { + _termsAccepted = _buttonsEnabled = true; + }); + }); + } + setState(() { + _termsAccepted = _buttonsEnabled = false; + }); + } + + void _onCloseTerms() { + Analytics.instance.logSelect(target: "Close Terms"); + Navigator.of(context).pop(); + } + + void _onClose() { + Analytics.instance.logSelect(target: "Close"); + Navigator.of(context).pop(false); + } + + void _onApprove() { + Analytics.instance.logSelect(target: "Approve"); + _submit(HealthFamilyMember.StatusAccepted); + } + + void _onDisapprove() { + Analytics.instance.logSelect(target: "Disapprove"); + _submit(HealthFamilyMember.StatusRevoked); + } + + void _submit(String status) { + if (_buttonsEnabled) { + setState(() { + _hasProgress = true; + _buttonsEnabled = false; + _errorMessage = (_errorMessage != null) ? '' : null; + }); + Health().applyFamilyMemberStatus(widget.pendingMember, status).then((dynamic result) { + if (mounted) { + if (result == true) { + setState(() { + _hasProgress = false; + }); + Navigator.of(context).pop(true); + } + else { + setState(() { + _hasProgress = false; + _buttonsEnabled = true; + _errorMessage = (result is String) ? result : Localization().getStringEx('panel.health.covid19.pending_family_member.label.text.error', 'Failed to submit.'); + }); + } + } + }); + } + } +} diff --git a/lib/ui/settings/SettingsPersonalInfoPanel.dart b/lib/ui/settings/SettingsPersonalInfoPanel.dart index 810af26c..1b86cbe8 100644 --- a/lib/ui/settings/SettingsPersonalInfoPanel.dart +++ b/lib/ui/settings/SettingsPersonalInfoPanel.dart @@ -43,13 +43,16 @@ class _SettingsPersonalInfoPanelState extends State { Future _deleteUserData() async{ Analytics.instance.logAlert(text: "Remove My Information", selection: "Yes"); - await Health().deleteUser(); - await Exposure().deleteUser(); bool piiDeleted = await Auth().deleteUserPiiData(); if(piiDeleted) { + await Health().deleteUser(); + await Exposure().deleteUser(); await User().deleteUser(); + // Auth().logout() - invoked by User().deleteUser() + } + else { + AppAlert.showDialogResult(context, Localization().getStringEx("panel.profile_info.header.title", "Failed to remove your information")); } - Auth().logout(); } diff --git a/pubspec.yaml b/pubspec.yaml index b64d4997..d338f866 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Illinois client application. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 2.9.12+912 +version: 2.9.18+918 environment: sdk: ">=2.2.0 <3.0.0"