generated from StanfordSpezi/SpeziTemplateApplication
-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
# Invitation Codes ## ♻️ Current situation & Problem There is a new collection in Firestore called invitation code bucket called `invitationCodes`. During onboarding, we sign the user in anonymously so we can check if they have a valid code. If they do, we assign that code to their new de-anonymized account and remove it from `invitationCodes` so it cannot be used again. Some kind of verification step like this hedges against spam and unapproved use. ## ⚙️ Release Notes - Adds an authentication step to onboarding whereby the user confirms against valid Firebase invitation codes that they are enrolled in the study. ## 📚 Documentation See [Firebase documentation](https://firebase.google.com/docs/auth/web/anonymous-auth). ## ✅ Testing TBD. ### Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md). --------- Co-authored-by: Paul Schmiedmayer <[email protected]>
- Loading branch information
1 parent
dfa0f6c
commit 0859e0e
Showing
26 changed files
with
7,364 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// | ||
// This source file is part of the PAWS application based on the Stanford Spezi Template Application project | ||
// | ||
// SPDX-FileCopyrightText: 2023 Stanford University | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
|
||
import Foundation | ||
|
||
|
||
enum InvitationCodeError: LocalizedError { | ||
case invitationCodeInvalid | ||
case userNotAuthenticated | ||
case generalError(String) | ||
|
||
var errorDescription: String? { | ||
switch self { | ||
case .invitationCodeInvalid: | ||
String(localized: "The invitation code is invalid or has already been used.", comment: "Invitation Code Invalid") | ||
case .userNotAuthenticated: | ||
String(localized: "User authentication failed. Please try to sign in again.", comment: "User Not Authenticated") | ||
case .generalError(let message): | ||
String(localized: "An error occurred: \(message)", comment: "General Error") | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
// | ||
// This source file is part of the PAWS application based on the Stanford Spezi Template Application project | ||
// | ||
// SPDX-FileCopyrightText: 2023 Stanford University | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
|
||
import Firebase | ||
import FirebaseAuth | ||
import FirebaseFunctions | ||
import SpeziOnboarding | ||
import SpeziValidation | ||
import SpeziViews | ||
import SwiftUI | ||
|
||
|
||
struct InvitationCodeView: View { | ||
@Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath | ||
@State private var invitationCode = "" | ||
@State private var viewState: ViewState = .idle | ||
@ValidationState private var validation | ||
|
||
|
||
var body: some View { | ||
ScrollView { | ||
VStack(spacing: 32) { | ||
invitationCodeHeader | ||
Divider() | ||
Grid(horizontalSpacing: 16, verticalSpacing: 16) { | ||
invitationCodeView | ||
} | ||
.padding(.top, -8) | ||
.padding(.bottom, -12) | ||
Divider() | ||
OnboardingActionsView( | ||
primaryText: "Redeem Invitation Code", | ||
primaryAction: { | ||
guard validation.validateSubviews() else { | ||
return | ||
} | ||
|
||
await verifyOnboardingCode() | ||
}, | ||
secondaryText: "I Already Have an Account", | ||
secondaryAction: { | ||
try Auth.auth().signOut() | ||
onboardingNavigationPath.nextStep() | ||
} | ||
) | ||
} | ||
.padding(.horizontal) | ||
.padding(.bottom) | ||
.viewStateAlert(state: $viewState) | ||
.navigationBarTitleDisplayMode(.large) | ||
.navigationTitle(String(localized: "Invitation Code")) | ||
} | ||
} | ||
|
||
|
||
@ViewBuilder private var invitationCodeView: some View { | ||
DescriptionGridRow { | ||
Text("Invitation Code") | ||
} content: { | ||
VerifiableTextField( | ||
LocalizedStringResource("Invitation Code"), | ||
text: $invitationCode | ||
) | ||
.autocorrectionDisabled(true) | ||
.textInputAutocapitalization(.characters) | ||
.textContentType(.oneTimeCode) | ||
.validate(input: invitationCode, rules: [invitationCodeValidationRule]) | ||
} | ||
.receiveValidation(in: $validation) | ||
} | ||
|
||
@ViewBuilder private var invitationCodeHeader: some View { | ||
VStack(spacing: 32) { | ||
Image(systemName: "rectangle.and.pencil.and.ellipsis") | ||
.resizable() | ||
.scaledToFit() | ||
.frame(height: 100) | ||
.accessibilityHidden(true) | ||
.foregroundStyle(Color.accentColor) | ||
Text("Please enter your invitation code to join the PAWS study.") | ||
} | ||
} | ||
|
||
private var invitationCodeValidationRule: ValidationRule { | ||
ValidationRule( | ||
rule: { invitationCode in | ||
invitationCode.count >= 8 | ||
}, | ||
message: "An invitation code is at least 8 characters long." | ||
) | ||
} | ||
|
||
init() { | ||
if FeatureFlags.useFirebaseEmulator { | ||
Functions.functions().useEmulator(withHost: "localhost", port: 5001) | ||
} | ||
} | ||
|
||
private func verifyOnboardingCode() async { | ||
do { | ||
if FeatureFlags.disableFirebase { | ||
guard invitationCode == "VASCTRAC" else { | ||
throw InvitationCodeError.invitationCodeInvalid | ||
} | ||
|
||
try? await Task.sleep(for: .seconds(0.25)) | ||
} else { | ||
if Auth.auth().currentUser == nil { | ||
async let authResult = Auth.auth().signInAnonymously() | ||
let checkInvitationCode = Functions.functions().httpsCallable("checkInvitationCode") | ||
|
||
do { | ||
_ = try await checkInvitationCode.call( | ||
[ | ||
"invitationCode": invitationCode, | ||
"userId": authResult.user.uid | ||
] | ||
) | ||
} catch { | ||
throw InvitationCodeError.invitationCodeInvalid | ||
} | ||
} | ||
} | ||
|
||
await onboardingNavigationPath.nextStep() | ||
} catch let error as NSError { | ||
if let errorCode = FunctionsErrorCode(rawValue: error.code) { | ||
// Handle Firebase-specific errors. | ||
switch errorCode { | ||
case .unauthenticated: | ||
viewState = .error(InvitationCodeError.userNotAuthenticated) | ||
case .notFound: | ||
viewState = .error(InvitationCodeError.invitationCodeInvalid) | ||
default: | ||
viewState = .error(InvitationCodeError.generalError(error.localizedDescription)) | ||
} | ||
} else { | ||
// Handle other errors, such as network issues or unexpected behavior. | ||
viewState = .error(InvitationCodeError.generalError(error.localizedDescription)) | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
||
#Preview { | ||
FirebaseApp.configure() | ||
|
||
return OnboardingStack { | ||
InvitationCodeView() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.