Skip to content

Commit

Permalink
Fix name not populating during sign-in with Apple (#59)
Browse files Browse the repository at this point in the history
  • Loading branch information
EvanCooper9 authored Nov 3, 2022
1 parent c8a1f5b commit 5b468a4
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -154,33 +154,22 @@ final class AuthenticationManager: NSObject, AuthenticationManaging {
private func listenForAuth() {
Auth.auth()
.authStateDidChangePublisher()
.sink(withUnretained: self) { strongSelf, firebaseUser in
.sinkAsync { [weak self] firebaseUser in
guard let firebaseUser = firebaseUser else {
strongSelf.currentUser = nil
strongSelf.userListener = nil
self?.currentUser = nil
self?.userListener = nil
return
}

Task {
try await self.updateFirestoreUserWithAuthUser(firebaseUser)
let user = try await self.database.document("users/\(firebaseUser.uid)").getDocument().decoded(as: User.self)
DispatchQueue.main.async {
self.currentUser = user
self._emailVerified.send(firebaseUser.isEmailVerified || firebaseUser.email == "[email protected]")
}
}
try await self?.updateFirestoreUserWithAuthUser(firebaseUser)
}
.store(in: &cancellables)
}

private func sha256(_ input: String) -> String {
let inputData = Data(input.utf8)
let hashedData = SHA256.hash(data: inputData)
let hashString = hashedData.compactMap {
return String(format: "%02x", $0)
}.joined()

return hashString
SHA256.hash(data: Data(input.utf8))
.compactMap { String(format: "%02x", $0) }
.joined()
}

private func registerUserManager(with user: User) {
Expand Down Expand Up @@ -211,53 +200,39 @@ final class AuthenticationManager: NSObject, AuthenticationManaging {
let user = User(id: firebaseUser.uid, email: email, name: name)
try await database.document("users/\(user.id)").setDataEncodable(user)
}

let user = try await self.database.document("users/\(firebaseUser.uid)").getDocument().decoded(as: User.self)
DispatchQueue.main.async {
self.currentUser = user
self._emailVerified.send(firebaseUser.isEmailVerified || firebaseUser.email == "[email protected]")
}
}
}

extension AuthenticationManager: ASAuthorizationControllerDelegate {
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential,
let tokenData = appleIDCredential.identityToken, let identityToken = String(data: tokenData, encoding: .utf8)
let tokenData = appleIDCredential.identityToken,
let idToken = String(data: tokenData, encoding: .utf8)
else { return }

let appleIDName = [appleIDCredential.fullName?.givenName, appleIDCredential.fullName?.familyName]
.compactMap { $0 }
.joined(separator: " ")

let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: identityToken, rawNonce: currentNonce)
let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: idToken, rawNonce: currentNonce)

// Sign in with Firebase.
Auth.auth().signIn(with: credential) { [weak self] authResult, error in
if let error {
// Error. If error.code == .MissingOrInvalidNonce, make sure
// you're sending the SHA256-hashed nonce as a hex string with
// your request to Apple.
print(error)
return
}

let name = [appleIDCredential.fullName?.givenName, appleIDCredential.fullName?.familyName]
.compactMap { $0 }
.joined(separator: " ")

let firebaseUser = Auth.auth().currentUser

let displayName = name.isEmpty ?
firebaseUser?.displayName ?? "" :
name

let changeRequest = firebaseUser?.createProfileChangeRequest()
changeRequest?.displayName = displayName
changeRequest?.commitChanges(completion: nil)

guard let firebaseUser = firebaseUser, let email = appleIDCredential.email ?? firebaseUser.email else { return }
let userJson = [
"id": firebaseUser.uid,
"email": email,
"name": displayName
]
self?.database.document("users/\(firebaseUser.uid)").updateData(userJson) { error in
guard let nsError = error as NSError?,
nsError.domain == "FIRFirestoreErrorDomain",
nsError.code == 5 else { return }
let user = User(id: firebaseUser.uid, email: email, name: displayName)
try? self?.database.document("users/\(user.id)").setDataEncodable(user)
guard let firebaseUser = authResult?.user else { return }
let displayName = appleIDName.nilIfEmpty ?? firebaseUser.displayName ?? ""
guard firebaseUser.displayName != displayName else { return }

let changeRequest = firebaseUser.createProfileChangeRequest()
changeRequest.displayName = displayName
changeRequest.commitChanges { [weak self] error in
Task { [weak self] in
try await self?.updateFirestoreUserWithAuthUser(firebaseUser)
}
}
}
}
Expand Down
2 changes: 0 additions & 2 deletions Friendly Competitions/Managers/FriendsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ final class FriendsManager: FriendsManaging {
.map { user, allFriends in
allFriends.filter { user.friends.contains($0.id) }
}
.print("friends")
.sink(withUnretained: self) { $0.friendsSubject.send($1) }
.store(in: &cancellables)

Expand All @@ -74,7 +73,6 @@ final class FriendsManager: FriendsManaging {
.map { user, allFriends in
allFriends.filter { user.incomingFriendRequests.contains($0.id) }
}
.print("friend requests")
.sink(withUnretained: self) { $0.friendRequestsSubject.send($1) }
.store(in: &cancellables)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,12 @@ final class ProfileViewModel: ObservableObject {
.assign(to: &$user)

_delete
.handleEvents(withUnretained: self, receiveOutput: { $0.loading = true })
.flatMapLatest(withUnretained: self) { strongSelf in
strongSelf.userManager
.deleteAccount()
.isLoading { [weak self] in self?.loading = $0 }
.ignoreFailure()
}
.handleEvents(withUnretained: self, receiveOutput: { $0.loading = false })
.sink()
.store(in: &cancellables)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ final class ExploreViewModel: ObservableObject {
// MARK: - Lifecycle

init() {
competitionsManager.appOwnedCompetitions
.print("app owned competitions")
.assign(to: &$appOwnedCompetitions)
competitionsManager.appOwnedCompetitions.assign(to: &$appOwnedCompetitions)

$searchText
.flatMapLatest(withUnretained: self) { strongSelf, searchText -> AnyPublisher<[Competition], Never> in
Expand Down
8 changes: 8 additions & 0 deletions firebase/firestore.indexes.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
{ "fieldPath": "isPublic", "order": "ASCENDING" },
{ "fieldPath": "owner", "order": "ASCENDING" }
]
},
{
"collectionGroup": "activitySummaries",
"queryScope": "COLLECTION_GROUP",
"fields": [
{ "fieldPath": "date", "order": "ASCENDING" },
{ "fieldPath": "userID", "order": "ASCENDING" }
]
}
],
"fieldOverrides": []
Expand Down
15 changes: 5 additions & 10 deletions firebase/functions/src/Handlers/account/deleteAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ async function deleteAccount(userID: string) {
(await firestore.collection("competitions").where("participants", "array-contains", userID).get())
.forEach(doc => {
const competition = new Competition(doc);
const participants = competition.participants;
participants.remove(userID);
const participants = competition.participants.filter(x => x != userID);
batch.update(firestore.doc(`competitions/${competition.id}`), {participants: participants});
batch.delete(firestore.doc(`competitions/${competition.id}/standings/${userID}`));
});
Expand All @@ -25,35 +24,31 @@ async function deleteAccount(userID: string) {
(await firestore.collection("competitions").where("pendingParticipants", "array-contains", userID).get())
.forEach(doc => {
const competition = new Competition(doc);
const pendingParticipants = competition.pendingParticipants;
pendingParticipants.remove(userID);
const pendingParticipants = competition.pendingParticipants.filter(x => x != userID);
batch.update(firestore.doc(`competitions/${competition.id}`), {pendingParticipants: pendingParticipants});
});

// remove from friends
(await firestore.collection("users").where("friends", "array-contains", userID).get())
.forEach(doc => {
const user = new User(doc);
const friends = user.friends;
friends.remove(userID);
const friends = user.friends.filter(x => x != userID);
batch.update(firestore.doc(`users/${user.id}`), {friends: friends});
});

// remove outgoing friend requests
(await firestore.collection("users").where("incomingFriendRequests", "array-contains", userID).get())
.forEach(doc => {
const user = new User(doc);
const incomingFriendRequests = user.incomingFriendRequests;
incomingFriendRequests.remove(userID);
const incomingFriendRequests = user.incomingFriendRequests.filter(x => x != userID);
batch.update(firestore.doc(`users/${user.id}`), {incomingFriendRequests: incomingFriendRequests});
});

// remove incoming friend requests
(await firestore.collection("users").where("outgoingFriendRequests", "array-contains", userID).get())
.forEach(doc => {
const user = new User(doc);
const outgoingFriendRequests = user.outgoingFriendRequests;
outgoingFriendRequests.remove(userID);
const outgoingFriendRequests = user.outgoingFriendRequests.filter(x => x != userID);
batch.update(firestore.doc(`users/${user.id}`), {outgoingFriendRequests: outgoingFriendRequests});
});

Expand Down

This file was deleted.

129 changes: 129 additions & 0 deletions firebase/functions/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import * as admin from "firebase-admin";
import * as functions from "firebase-functions";
import * as moment from "moment";
import { deleteAccount } from "./Handlers/account/deleteAccount";
import { deleteCompetition } from "./Handlers/competitions/deleteCompetition";
import { updateCompetitionStandings } from "./Handlers/competitions/updateCompetitionStandings";
import { deleteFriend } from "./Handlers/friends/deleteFriend";
import { FriendRequestAction, handleFriendRequest } from "./Handlers/friends/handleFriendRequest";
import { ActivitySummary } from "./Models/ActivitySummary";
import { Competition } from "./Models/Competition";
import { Standing } from "./Models/Standing";
import { User } from "./Models/User";
import * as notifications from "./notifications";
import { getFirestore } from "./Utilities/firstore";

admin.initializeApp();
const firestore = getFirestore();

// Account

exports.deleteAccount = functions.https.onCall(async (_data, context) => {
const userID = context.auth?.uid;
if (userID == null) return;
await deleteAccount(userID);
});

// Competitions

exports.deleteCompetition = functions.https.onCall(data => {
const competitionID = data.competitionID;
return deleteCompetition(competitionID);
});

exports.updateCompetitionStandings = functions.https.onCall((_data, context) => {
const userID = context.auth?.uid;
if (userID == null) return Promise.resolve();
return updateCompetitionStandings(userID);
});

// Friends

exports.sendFriendRequest = functions.https.onCall((data, context) => {
const requesterID = context.auth?.uid;
const requesteeID = data.userID;
if (requesterID == null) return Promise.resolve();
return handleFriendRequest(requesterID, requesteeID, FriendRequestAction.create);
});

exports.respondToFriendRequest = functions.https.onCall((data, context) => {
const requesterID = context.auth?.uid;
const requesteeID = data.userID;
const accept = data.accept;
if (requesterID == null) return Promise.resolve();
return handleFriendRequest(requesterID, requesteeID, accept ? FriendRequestAction.accept : FriendRequestAction.decline);
});

exports.deleteFriend = functions.https.onCall((data, context) => {
const userID = context.auth?.uid;
const friendID = data.userID;
if (userID == null) return Promise.resolve();
return deleteFriend(userID, friendID);
});

// Jobs

exports.cleanStaleActivitySummaries = functions.pubsub.schedule("every day 02:00")
.timeZone("America/Toronto")
.onRun(async () => {
const users = (await firestore.collection("users").get()).docs.map(doc => new User(doc));
const competitions = (await firestore.collection("competitions").get()).docs.map(doc => new Competition(doc));
const cleanUsers = users.map(async user => {
const participatingCompetitions = competitions.filter(competition => competition.participants.includes(user.id));
const activitySummariesToDelete = (await firestore.collection(`users/${user.id}/activitySummaries`).get())
.docs
.filter(doc => {
const activitySummary = new ActivitySummary(doc);
const matchingCompetition = participatingCompetitions.find(competition => activitySummary.isIncludedInCompetition(competition));
return matchingCompetition == null || matchingCompetition == undefined;
})
.map(doc => firestore.doc(`users/${user.id}/activitySummaries/${doc.id}`).delete());

return Promise.all(activitySummariesToDelete);
});

return Promise.all(cleanUsers);
});

exports.sendCompetitionCompleteNotifications = functions.pubsub.schedule("every day 12:00")
.timeZone("America/Toronto")
.onRun(async () => {
const competitionsRef = await firestore.collection("competitions").get();
const competitionPromises = competitionsRef.docs.map(async doc => {
const competition = new Competition(doc);

const competitionEnd = moment(competition.end);
const yesterday = moment().utc().subtract(1, "day");
if (yesterday.dayOfYear() != competitionEnd.dayOfYear() || yesterday.year() != competitionEnd.year()) return;

const standingsRef = await firestore.collection(`competitions/${competition.id}/standings`).get();
const standings = standingsRef.docs.map(doc => new Standing(doc));
const notificationPromises = competition.participants
.filter(participantId => !participantId.startsWith("Anonymous"))
.map(async participantId => {
const userRef = await firestore.doc(`users/${participantId}`).get();
const user = new User(userRef);
const standing = standings.find(standing => standing.userId == user.id);
if (standing == null) return null;

const rank = standing.rank;
const ordinal = ["st", "nd", "rd"][((rank+90)%100-10)%10-1] || "th";
return notifications
.sendNotificationsToUser(
user,
"Competition complete!",
`You placed ${rank}${ordinal} in ${competition.name}!`
)
.then(() => user.updateStatisticsWithNewRank(rank));
});

return Promise
.all(notificationPromises)
.then(async () => {
await competition.updateRepeatingCompetition();
await competition.updateStandings();
});
});

return Promise.all(competitionPromises);
});

0 comments on commit 5b468a4

Please sign in to comment.