├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── SPProfiling │ ├── Data │ ├── Constants.swift │ ├── Texts.swift │ └── UserDefaultsExtension.swift │ ├── Interface │ ├── Auth │ │ ├── AuthController.swift │ │ ├── AuthOnboardingController.swift │ │ └── AuthToolBarView.swift │ ├── Devices │ │ ├── DeviceTableCell.swift │ │ └── DevicesController.swift │ ├── NativeAvatarViewExtension.swift │ └── Profile │ │ ├── ProfileController+Internal.swift │ │ ├── ProfileController+UITextFieldDelegate.swift │ │ ├── ProfileController.swift │ │ └── Table │ │ ├── CellProvider+Profile.swift │ │ ├── DiffableProfileItem.swift │ │ └── ProfileTableViewCell.swift │ ├── Models │ ├── AuthWay.swift │ ├── Errors │ │ ├── AuthError.swift │ │ ├── DeleteAvatarError.swift │ │ ├── DeleteProfileError.swift │ │ ├── EditProfileError.swift │ │ ├── GetAvatarError.swift │ │ ├── GetProfileError.swift │ │ └── SetAvatarError.swift │ ├── Middleware │ │ ├── ProfileMiddlewareError.swift │ │ └── ProfileModel+Middleware.swift │ ├── ProfileAction.swift │ ├── ProfileDeviceModel.swift │ └── ProfileModel.swift │ ├── Notifications.swift │ ├── ProfileModelExtension.swift │ ├── Resources │ └── Localization │ │ ├── en.lproj │ │ └── Localizable.strings │ │ └── ru.lproj │ │ └── Localizable.strings │ ├── SPProfiling.swift │ └── Services │ ├── Auth.swift │ ├── Profile+Actions.swift │ ├── Profile+Avatar.swift │ ├── Profile+Convertor.swift │ ├── Profile+Device.swift │ ├── Profile+Firebase.swift │ ├── Profile+Observer.swift │ └── Profile.swift └── TODO.md /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: ivanvorobei 7 | --- 8 | 9 | **Details** 10 | - iOS Version [e.g. 15.2] 11 | - Framework Version [e.g. 1.0.2] 12 | - Installed via [e.g. SPM, Cocoapods, Manually] 13 | 14 | **Describe the Bug** 15 | A clear and concise description of what the bug is. 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: ivanvorobei 7 | 8 | --- 9 | 10 | **Feature Description** 11 | Describe what functionality you want to see. 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Something is not clear with the project 4 | title: '' 5 | labels: question 6 | assignees: ivanvorobei 7 | 8 | --- 9 | 10 | **Describe the Problem** 11 | A clear and concise description of what you want to do. 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Goal 2 | 3 | 4 | ## Checklist 5 | 6 | - [] Testing in compability platforms 7 | - [] Installed correct via `Swift Package Manager` and `Cocoapods` 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # osX files 2 | .DS_Store 3 | .Trashes 4 | 5 | # Swift Package Manager 6 | .swiftpm 7 | 8 | # User Interface 9 | *UserInterfaceState.xcuserstate 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ivan Vorobei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.4 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SPProfiling", 7 | defaultLocalization: "en", 8 | platforms: [ 9 | .iOS(.v13) 10 | ], 11 | products: [ 12 | .library( 13 | name: "SPProfiling", 14 | targets: ["SPProfiling"] 15 | ) 16 | ], 17 | dependencies: [ 18 | .package(url: "https://github.com/ivanvorobei/SPAlert", .upToNextMajor(from: "4.2.0")), 19 | .package(url: "https://github.com/ivanvorobei/NativeUIKit", .upToNextMajor(from: "1.4.6")), 20 | .package(url: "https://github.com/ivanvorobei/SPFirebase", .upToNextMajor(from: "1.0.9")), 21 | .package(url: "https://github.com/sparrowcode/SPSafeSymbols", .upToNextMajor(from: "1.0.5")), 22 | .package(url: "https://github.com/kean/Nuke", .upToNextMajor(from: "10.7.1")) 23 | ], 24 | targets: [ 25 | .target( 26 | name: "SPProfiling", 27 | dependencies: [ 28 | .product(name: "SPAlert", package: "SPAlert"), 29 | .product(name: "NativeUIKit", package: "NativeUIKit"), 30 | .product(name: "SPFirebaseAuth", package: "SPFirebase"), 31 | .product(name: "SPFirebaseFirestore", package: "SPFirebase"), 32 | .product(name: "SPFirebaseMessaging", package: "SPFirebase"), 33 | .product(name: "SPFirebaseStorage", package: "SPFirebase"), 34 | .product(name: "SPSafeSymbols", package: "SPSafeSymbols"), 35 | .product(name: "Nuke", package: "Nuke") 36 | ], 37 | resources: [ 38 | .process("Resources") 39 | ] 40 | ) 41 | ] 42 | ) 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SPProfiling 2 | 3 | Ready use service with using Firebase. Included interface, manage auth process, recored devices and profile data. 4 | 5 | 6 | 7 | ## Installation 8 | 9 | Ready for use on iOS 13+. 10 | 11 | ### Swift Package Manager 12 | 13 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. It’s integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies. 14 | 15 | Once you have your Swift package set up, adding as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. 16 | 17 | ```swift 18 | dependencies: [ 19 | .package(url: "https://github.com/ivanvorobei/SPProfiling", .upToNextMajor(from: "1.0.2")) 20 | ] 21 | ``` 22 | 23 | ### Manually 24 | 25 | If you prefer not to use any of dependency managers, you can integrate manually. Put `Sources/SPProfiling` folder in your Xcode project. Make sure to enable `Copy items if needed` and `Create groups`. 26 | 27 | ## Usage 28 | 29 | First call configure services: 30 | 31 | ```swift 32 | let filePath = Bundle.main.path(forResource: Constants.Firebase.plist_filename, ofType: .empty)! 33 | let options = FirebaseOptions(contentsOfFile: filePath)! 34 | SPProfiling.configure(firebaseOptions: options) 35 | ``` 36 | 37 | All actions doing from `ProfileModel`. 38 | 39 | ```swift 40 | ProfileModel.isAuthed 41 | ProfileModel.isAnonymous 42 | ProfileModel.currentProfile 43 | 44 | ProfileModel.getProfile(userID...) 45 | ProfileModel.getProfile(email...) 46 | 47 | ProfileModel.signInApple(...) 48 | ProfileModel.signInAnonymously(...) 49 | ProfileModel.signOut(...) 50 | 51 | let profileModel = ProfileModel.currentProfile 52 | profileModel.setName(...) 53 | profileModel.getAvatarURL(...) 54 | profileModel.setAvatar(...) 55 | profileModel.deleteAvatar(...) 56 | 57 | // Ready-use interface 58 | ProfileModel.showCurrentProfile(...) 59 | ``` 60 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Data/Constants.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | 24 | enum Constants { 25 | 26 | enum Firebase { 27 | 28 | // MARK: Storage 29 | 30 | enum Storage { 31 | 32 | enum Paths { 33 | 34 | static var profiles: String { "profiles" } 35 | static var avatar_filename: String { "avatar.jpg" } 36 | } 37 | } 38 | 39 | // MARK: Firestore 40 | 41 | enum Firestore { 42 | 43 | enum Paths { 44 | 45 | static var profiles: String { "profiles" } 46 | static var homes: String { "homes" } 47 | static var checklists: String { "checklists" } 48 | } 49 | 50 | enum Fields { 51 | 52 | public enum Profile { 53 | 54 | static var profileName: String { "name" } 55 | static var profileEmail: String { "email" } 56 | static var profileDevices: String { "devices" } 57 | static var profileCreatedDate: String { "created" } 58 | 59 | static var deviceID: String { "id" } 60 | static var deviceName: String { "name" } 61 | static var deviceType: String { "type" } 62 | static var deviceFCMToken: String { "fcmToken" } 63 | static var deviceLanguageCode: String { "language" } 64 | static var deviceAddedDate: String { "added" } 65 | } 66 | 67 | public enum Home { 68 | 69 | static var name: String { "name" } 70 | static var usersIDs: String { "usersIDs" } 71 | static var users: String { "users" } 72 | static var createdDate: String { "created" } 73 | } 74 | 75 | public enum HomeUser { 76 | 77 | static var access: String { "access" } 78 | static var invited: String { "invited" } 79 | static var inviteDate: String { "inviteDate" } 80 | static var inviteAcceptedDate: String { "inviteAcceptedDate" } 81 | static var invitedByUserID: String { "invitedByUserID" } 82 | } 83 | 84 | public enum Checklist { 85 | 86 | static var checklistTitle: String { "title" } 87 | static var checklistIcon: String { "icon" } 88 | static var checklistColor: String { "color" } 89 | static var cheklistSections: String { "sections" } 90 | static var checklistCreatedDate: String { "created" } 91 | 92 | static var sectionIndex: String { "index" } 93 | static var sectionTitle: String { "title" } 94 | static var sectionActions: String { "actions" } 95 | 96 | static var actionIndex: String { "index" } 97 | static var actionTitle: String { "title" } 98 | static var actionCompleteState: String { "completeState" } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Data/Texts.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | 24 | enum Texts { 25 | 26 | public static var save: String { NSLocalizedString("save", bundle: .module, comment: "") } 27 | public static var cancel: String { NSLocalizedString("cancel", bundle: .module, comment: "") } 28 | 29 | // MARK: - Error 30 | 31 | enum Error { 32 | 33 | static var not_found: String { NSLocalizedString("error not found", bundle: .module, comment: "") } 34 | static var not_enough_permissions: String { NSLocalizedString("error not enough permissions", bundle: .module, comment: "") } 35 | static var not_reachable: String { NSLocalizedString("error not reachable", bundle: .module, comment: "") } 36 | static var unknown: String { NSLocalizedString("error unknown", bundle: .module, comment: "") } 37 | 38 | enum Auth { 39 | 40 | static var canceled: String { NSLocalizedString("error auth canceled", bundle: .module, comment: "") } 41 | static var cant_present: String { NSLocalizedString("error auth cant present", bundle: .module, comment: "") } 42 | static var cant_prepare_required_data: String { NSLocalizedString("error auth cant prepare required data", bundle: .module, comment: "") } 43 | } 44 | 45 | enum Profile { 46 | 47 | static var name_short: String { NSLocalizedString("error profile name short", bundle: .module, comment: "") } 48 | static var name_long: String { NSLocalizedString("error profile name long", bundle: .module, comment: "") } 49 | static var empty_name: String { NSLocalizedString("error profile empty name", bundle: .module, comment: "") } 50 | static var big_avatar_width: String { NSLocalizedString("error profile big avatar width", bundle: .module, comment: "") } 51 | static var big_avatar_size: String { NSLocalizedString("error profile big avatar size", bundle: .module, comment: "") } 52 | } 53 | } 54 | 55 | // MARK: - Auth 56 | 57 | enum Auth { 58 | 59 | static var sign_in: String { NSLocalizedString("auth sign in", bundle: .module, comment: "") } 60 | static var continue_anonymously: String { NSLocalizedString("auth continue anonymously", bundle: .module, comment: "") } 61 | static var description: String { NSLocalizedString("auth description", bundle: .module, comment: "") } 62 | static var footer_description: String { NSLocalizedString("auth footer description", bundle: .module, comment: "") } 63 | } 64 | 65 | // MARK: - Profile 66 | 67 | enum Profile { 68 | 69 | static var name_title: String { NSLocalizedString("profile name title", bundle: .module, comment: "") } 70 | static var email_title: String { NSLocalizedString("profile email title", bundle: .module, comment: "") } 71 | static var placeholder_name: String { NSLocalizedString("profile placeholder name", bundle: .module, comment: "") } 72 | 73 | static var public_data_header: String { NSLocalizedString("profile public data header", bundle: .module, comment: "") } 74 | static var public_data_footer: String { NSLocalizedString("profile public data footer", bundle: .module, comment: "") } 75 | 76 | enum Devices { 77 | 78 | static var title: String { NSLocalizedString("profile devices title", bundle: .module, comment: "") } 79 | static var header: String { NSLocalizedString("profile devices header", bundle: .module, comment: "") } 80 | static var footer: String { NSLocalizedString("profile devices footer", bundle: .module, comment: "") } 81 | 82 | static var manage_devices: String { NSLocalizedString("profile devices manage devices", bundle: .module, comment: "") } 83 | 84 | static func added_date(date: Date) -> String { 85 | let localisedDate = date.formatted(dateStyle: .medium) 86 | return String(format: NSLocalizedString("profile devices added date", bundle: .module, comment: ""), localisedDate) 87 | } 88 | } 89 | 90 | enum Actions { 91 | 92 | enum SignOut { 93 | 94 | static var title: String { NSLocalizedString("profile actions sign out title", bundle: .module, comment: "") } 95 | static var description: String { NSLocalizedString("profile actions sign out description", bundle: .module, comment: "") } 96 | 97 | enum Confirm { 98 | 99 | static var title: String { NSLocalizedString("profile actions sign out confirm title", bundle: .module, comment: "") } 100 | static var description: String { NSLocalizedString("profile actions sign out confirm description", bundle: .module, comment: "") } 101 | } 102 | } 103 | 104 | enum Delete { 105 | 106 | static var title: String { NSLocalizedString("profile actions delete title", bundle: .module, comment: "") } 107 | static var header: String { NSLocalizedString("profile actions delete header", bundle: .module, comment: "") } 108 | static var description: String { NSLocalizedString("profile actions delete description", bundle: .module, comment: "") } 109 | 110 | enum Confirm { 111 | 112 | static var title: String { NSLocalizedString("profile actions delete confirm title", bundle: .module, comment: "") } 113 | static var description: String { NSLocalizedString("profile actions delete confirm description", bundle: .module, comment: "") } 114 | } 115 | } 116 | } 117 | 118 | enum Avatar { 119 | 120 | static var open_camera: String { NSLocalizedString("profile avatar open camera", bundle: .module, comment: "") } 121 | static var open_photo_library: String { NSLocalizedString("profile avatar open photo library", bundle: .module, comment: "") } 122 | static var delete_action_title: String { NSLocalizedString("profile avatar delete action title", bundle: .module, comment: "") } 123 | static var actions_description: String { NSLocalizedString("profile avatar actions description", bundle: .module, comment: "") } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Data/UserDefaultsExtension.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | import Foundation 22 | 23 | extension UserDefaults { 24 | 25 | enum Values { 26 | 27 | public static var firebase_fcm_token: String? { 28 | get { UserDefaults.standard.string(forKey: Keys.firebase_fcm_token) } 29 | set { UserDefaults.standard.setValue(newValue, forKey: Keys.firebase_fcm_token) } 30 | } 31 | } 32 | 33 | enum Keys { 34 | 35 | static var firebase_fcm_token: String { "sprofiling_firebase_fcm_token" } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Interface/Auth/AuthController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import NativeUIKit 24 | import SparrowKit 25 | import SPAlert 26 | 27 | open class AuthController: NativeOnboardingFeaturesController { 28 | 29 | public var completion: ((AuthController)->Void)? 30 | 31 | // MARK: - Views 32 | 33 | public let actionToolbarView = AuthToolBarView() 34 | 35 | // MARK: - Init 36 | 37 | public init(title: String, description: String, completion: ((AuthController)->Void)? = nil) { 38 | super.init( 39 | iconImage: NativeAvatarView.generatePlaceholderImage(fontSize: 80, fontWeight: .medium), 40 | title: title, 41 | subtitle: description 42 | ) 43 | self.completion = completion 44 | } 45 | 46 | public required init?(coder: NSCoder) { 47 | fatalError("init(coder:) has not been implemented") 48 | } 49 | 50 | deinit { 51 | NotificationCenter.default.removeObserver(self) 52 | } 53 | 54 | // MARK: - Lifecycle 55 | 56 | public override func viewDidLoad() { 57 | super.viewDidLoad() 58 | 59 | if let navigationController = self.navigationController as? NativeNavigationController { 60 | navigationController.mimicrateToolBarView = actionToolbarView 61 | } 62 | 63 | actionToolbarView.authButton.addTarget(self, action: #selector(self.tapAppleSignIn), for: .touchUpInside) 64 | actionToolbarView.skipAuthButton.addTarget(self, action: #selector(self.tapContinueAnon), for: .touchUpInside) 65 | 66 | NotificationCenter.default.addObserver(self, selector: #selector(self.updateSkipAuthButton), name: SPProfiling.didChangedAuthState, object: nil) 67 | 68 | updateSkipAuthButton() 69 | } 70 | 71 | // MARK: - Actions 72 | 73 | @objc func tapAppleSignIn() { 74 | self.actionToolbarView.setLoading(true) 75 | ProfileModel.signInApple(on: self) { error in 76 | if let error = error { 77 | self.actionToolbarView.setLoading(false) 78 | SPAlert.present(message: error.localizedDescription, haptic: .error) 79 | } else { 80 | self.completion?(self) 81 | } 82 | } 83 | } 84 | 85 | @objc func tapContinueAnon() { 86 | if ProfileModel.isAnonymous ?? false { 87 | self.completion?(self) 88 | } else { 89 | self.actionToolbarView.setLoading(true) 90 | ProfileModel.signInAnonymously() { error in 91 | if let error = error { 92 | self.actionToolbarView.setLoading(false) 93 | SPAlert.present(message: error.localizedDescription, haptic: .error) 94 | } else { 95 | self.completion?(self) 96 | } 97 | } 98 | } 99 | } 100 | 101 | // MARK: - Private 102 | 103 | @objc func updateSkipAuthButton() { 104 | let allowed: Bool = { 105 | switch SPProfiling.authWay { 106 | case .onlyAuthed: 107 | return false 108 | case .anonymouslyAllowed: 109 | if ProfileModel.isAnonymous != nil { 110 | // Any auth already isset. 111 | // Not allowed anonymous auth. 112 | return false 113 | } else { 114 | return true 115 | } 116 | } 117 | }() 118 | actionToolbarView.skipAuthButton.isHidden = !allowed 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Interface/Auth/AuthOnboardingController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import NativeUIKit 24 | import SparrowKit 25 | import SPAlert 26 | 27 | open class AuthOnboardingController: AuthController, NativeOnboardingChildInterface { 28 | 29 | public weak var onboardingManagerDelegate: NativeOnboardingManagerDelegate? 30 | 31 | // MARK: - Init 32 | 33 | public init(title: String, description: String) { 34 | super.init( 35 | title: title, 36 | description: description 37 | ) 38 | self.completion = { controller in 39 | self.onboardingManagerDelegate?.onboardingActionComplete(for: controller) 40 | } 41 | } 42 | 43 | public required init?(coder: NSCoder) { 44 | fatalError("init(coder:) has not been implemented") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Interface/Auth/AuthToolBarView.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import SparrowKit 24 | import NativeUIKit 25 | import AuthenticationServices 26 | 27 | open class AuthToolBarView: NativeMimicrateToolBarView { 28 | 29 | // MARK: - Views 30 | 31 | public let activityIndicatorView = UIActivityIndicatorView() 32 | 33 | public let authButton = ASAuthorizationAppleIDButton().do { 34 | $0.roundCorners(radius: NativeLargeActionButton.defaultCornerRadius) 35 | $0.layer.masksToBounds = true 36 | } 37 | 38 | public let skipAuthButton = SPDimmedButton().do { 39 | $0.setTitle(Texts.Auth.continue_anonymously) 40 | $0.applyDefaultAppearance(with: .tintedContent) 41 | $0.titleLabel?.font = UIFont.preferredFont(forTextStyle: .headline, addPoints: -1) 42 | } 43 | 44 | // MARK: - Init 45 | 46 | open override func commonInit() { 47 | super.commonInit() 48 | addSubview(activityIndicatorView) 49 | addSubview(authButton) 50 | addSubview(skipAuthButton) 51 | } 52 | 53 | // MARK: - Actions 54 | 55 | open func setLoading(_ state: Bool) { 56 | if state { 57 | activityIndicatorView.startAnimating() 58 | authButton.alpha = .zero 59 | skipAuthButton.alpha = .zero 60 | } else { 61 | activityIndicatorView.stopAnimating() 62 | authButton.alpha = 1 63 | skipAuthButton.alpha = 1 64 | } 65 | } 66 | 67 | // MARK: - Layout 68 | 69 | open override func layoutSubviews() { 70 | super.layoutSubviews() 71 | 72 | let authButtonWidth = min(readableWidth, NativeLayout.Sizes.actionable_area_maximum_width) 73 | authButton.frame.setWidth(authButtonWidth) 74 | authButton.frame.setHeight(NativeLargeActionButton.defaultHeight) 75 | authButton.setXCenter() 76 | authButton.frame.origin.y = layoutMargins.top 77 | 78 | skipAuthButton.setWidthAndFit(width: layoutWidth) 79 | skipAuthButton.frame.origin.y = authButton.frame.maxY + 12 80 | skipAuthButton.setXCenter() 81 | 82 | let contentHeight: CGFloat = skipAuthButton.frame.maxY 83 | activityIndicatorView.setXCenter() 84 | activityIndicatorView.center.y = contentHeight / 2 85 | } 86 | 87 | open override func sizeThatFits(_ size: CGSize) -> CGSize { 88 | layoutSubviews() 89 | if skipAuthButton.isHidden { 90 | return .init(width: size.width, height: authButton.frame.maxY + layoutMargins.bottom) 91 | } else { 92 | return .init(width: size.width, height: skipAuthButton.frame.maxY + layoutMargins.bottom) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Interface/Devices/DeviceTableCell.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import SparrowKit 24 | import SPDiffable 25 | 26 | class DeviceTableCell: SPTableViewCell { 27 | 28 | // MARK: - Init 29 | 30 | public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 31 | super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) 32 | } 33 | 34 | required init?(coder aDecoder: NSCoder) { 35 | fatalError("init(coder:) has not been implemented") 36 | } 37 | 38 | override func commonInit() { 39 | super.commonInit() 40 | textLabel?.font = UIFont.preferredFont(forTextStyle: .body, weight: .medium) 41 | textLabel?.textColor = .label 42 | textLabel?.numberOfLines = .zero 43 | detailTextLabel?.font = UIFont.preferredFont(forTextStyle: .footnote) 44 | detailTextLabel?.textColor = .secondaryLabel 45 | detailTextLabel?.numberOfLines = .zero 46 | higlightStyle = .content 47 | } 48 | 49 | // MARK: - Public 50 | 51 | func setDevice(_ model: ProfileDeviceModel) { 52 | textLabel?.text = model.name 53 | detailTextLabel?.text = Texts.Profile.Devices.added_date(date: model.addedDate) 54 | let font = UIFont.preferredFont(forTextStyle: .title3, weight: .medium) 55 | 56 | imageView?.image = { 57 | switch model.type { 58 | case .phone: 59 | return UIImage.system("iphone", font: font) 60 | case .pad: 61 | return UIImage.system("ipad", font: font) 62 | case .desktop: 63 | return UIImage.system("laptopcomputer", font: font) 64 | } 65 | }() 66 | 67 | imageView?.tintColor = .tint 68 | } 69 | 70 | // MARK: - Layout 71 | 72 | open override func sizeThatFits(_ size: CGSize) -> CGSize { 73 | let superSize = super.sizeThatFits(size) 74 | return .init(width: superSize.width, height: superSize.height + 2) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Interface/Devices/DevicesController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | 23 | import UIKit 24 | import SparrowKit 25 | import SPDiffable 26 | 27 | class DevicesController: SPDiffableTableController { 28 | 29 | internal var profileModel = ProfileModel.currentProfile 30 | 31 | // MARK: - Init 32 | 33 | init() { 34 | super.init(style: .insetGrouped) 35 | } 36 | 37 | required init?(coder: NSCoder) { 38 | fatalError("init(coder:) has not been implemented") 39 | } 40 | 41 | // MARK: - Lifecycle 42 | 43 | override func viewDidLoad() { 44 | super.viewDidLoad() 45 | navigationItem.title = Texts.Profile.Devices.title 46 | tableView.register(DeviceTableCell.self) 47 | 48 | configureDiffable(sections: content, cellProviders: [ 49 | .init(clouser: { tableView, indexPath, item in 50 | guard let deviceModel = (item as? SPDiffableWrapperItem)?.model as? ProfileDeviceModel else { return nil } 51 | let cell = self.tableView.dequeueReusableCell(withClass: DeviceTableCell.self, for: indexPath) 52 | cell.textLabel?.text = deviceModel.name 53 | cell.detailTextLabel?.text = deviceModel.addedDate.formatted(dateStyle: .medium) 54 | return cell 55 | }) 56 | ]) 57 | 58 | NotificationCenter.default.addObserver(forName: SPProfiling.didReloadedProfile, object: nil, queue: nil) { [weak self] _ in 59 | guard let self = self else { return } 60 | if let profileModel = ProfileModel.currentProfile { 61 | self.profileModel = profileModel 62 | self.diffableDataSource?.set(self.content, animated: true) 63 | } 64 | } 65 | } 66 | 67 | // MARK: - Diffable 68 | 69 | enum Section: String { 70 | 71 | case devices 72 | 73 | var id: String { rawValue } 74 | } 75 | 76 | internal var content: [SPDiffableSection] { 77 | 78 | let devices = profileModel?.devices ?? [] 79 | 80 | return [ 81 | .init( 82 | id: Section.devices.id, 83 | header: SPDiffableTextHeaderFooter(text: Texts.Profile.Devices.header), 84 | footer: SPDiffableTextHeaderFooter(text: Texts.Profile.Devices.footer), 85 | items: devices.map({ SPDiffableWrapperItem(id: $0.id, model: $0) }) 86 | ) 87 | ] 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Interface/NativeAvatarViewExtension.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import NativeUIKit 24 | import SparrowKit 25 | import Nuke 26 | 27 | extension NativeAvatarView: Nuke_ImageDisplaying { 28 | 29 | public func setAvatar(of profileModel: ProfileModel, completion: (()->Void)? = nil) { 30 | avatarAppearance = .loading 31 | profileModel.getAvatarURL { [weak self] url, error in 32 | guard let self = self else { return } 33 | var options = ImageLoadingOptions() 34 | options.contentModes = .init(success: .scaleAspectFill, failure: .scaleAspectFill, placeholder: .scaleAspectFill) 35 | loadImage(with: url, options: options, into: self, progress: nil) { result in 36 | switch result { 37 | case .success(let response): 38 | self.avatarAppearance = .avatar(response.image) 39 | case .failure(_): 40 | self.avatarAppearance = .placeholder 41 | } 42 | completion?() 43 | } 44 | } 45 | } 46 | 47 | open func nuke_display(image: UIImage?, data: Data?) { 48 | if let image = image { 49 | avatarAppearance = .avatar(image) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Interface/Profile/ProfileController+Internal.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import NativeUIKit 24 | import SparrowKit 25 | import SPDiffable 26 | import SPSafeSymbols 27 | import SPAlert 28 | 29 | extension ProfileController { 30 | 31 | internal func configureHeader() { 32 | headerView.avatarView.setAvatar(of: profileModel) { [weak self] in 33 | guard let self = self else { return } 34 | self.configureAvatarActions() 35 | } 36 | } 37 | 38 | internal func configureAvatarActions() { 39 | 40 | let processPickedImage: ((UIImage) -> Void) = { image in 41 | self.headerView.avatarView.avatarAppearance = .loading 42 | self.profileModel.setAvatar(image) { [weak self] error in 43 | if let error = error { 44 | SPAlert.present(message: error.localizedDescription, haptic: .error, completion: nil) 45 | } 46 | self?.configureHeader() 47 | } 48 | } 49 | 50 | var actions: [UIAction] = [] 51 | let openCameraAction = UIAction.init(title: Texts.Profile.Avatar.open_camera, image: .init(.camera), handler: { [weak self] _ in 52 | guard let self = self else { return } 53 | SPImagePicker.pick(sourceType: .camera, on: self) { pickedImage in 54 | if let image = pickedImage { 55 | processPickedImage(image) 56 | } 57 | } 58 | }) 59 | actions.append(openCameraAction) 60 | 61 | let openPhotoLibraryAction = UIAction.init(title: Texts.Profile.Avatar.open_photo_library, image: .init(.photo), handler: { [weak self] _ in 62 | guard let self = self else { return } 63 | SPImagePicker.pick(sourceType: .photoLibrary, on: self) { pickedImage in 64 | if let image = pickedImage { 65 | processPickedImage(image) 66 | } 67 | } 68 | }) 69 | actions.append(openPhotoLibraryAction) 70 | 71 | if headerView.avatarView.hasAvatar { 72 | let removeAvatarAction = UIAction.init(title: Texts.Profile.Avatar.delete_action_title, image: .init(.trash), attributes: [.destructive]) { [weak self] _ in 73 | guard let self = self else { return } 74 | self.headerView.avatarView.avatarAppearance = .loading 75 | self.profileModel.deleteAvatar { [weak self] error in 76 | if let error = error { 77 | SPAlert.present(message: error.localizedDescription, haptic: .error) 78 | } else { 79 | guard let self = self else { return } 80 | self.configureHeader() 81 | } 82 | } 83 | } 84 | actions.append(removeAvatarAction) 85 | } 86 | 87 | if #available(iOS 14.0, *) { 88 | let menu = UIMenu.init(title: Texts.Profile.Avatar.actions_description, children: actions) 89 | for button in [headerView.avatarView.indicatorButton, headerView.avatarView.placeholderButton, headerView.avatarView.avatarButton] { 90 | button.showsMenuAsPrimaryAction = true 91 | button.menu = menu 92 | } 93 | } else { 94 | #warning("add support ios 13 with alert") 95 | } 96 | 97 | headerView.avatarView.isEditable = true 98 | } 99 | /* 100 | internal func setProfile(_ profileModel: ProfileModel, completion: (()->())? = nil) { 101 | headerView.nameLabel.text = profileModel.name 102 | headerView.namePlaceholderLabel.text = Texts.Profile.placeholder_name 103 | if let email = profileModel.email { headerView.emailButton.setTitle(email) } 104 | headerView.layoutSubviews() 105 | 106 | headerView.emailButton.removeTargetsAndActions() 107 | headerView.emailButton.addTarget(self, action: #selector(didTapEmailButton), for: .touchUpInside) 108 | 109 | headerView.avatarView.setAvatar(of: profileModel) { 110 | completion?() 111 | } 112 | } 113 | 114 | @objc func didTapEmailButton() { 115 | SPAlert.present(title: Texts.Profile.Actions.email_copied, message: nil, preset: .done, completion: nil) 116 | UIPasteboard.general.string = profileModel.email 117 | }*/ 118 | } 119 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Interface/Profile/ProfileController+UITextFieldDelegate.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import NativeUIKit 24 | import SparrowKit 25 | import SPDiffable 26 | import SPSafeSymbols 27 | import SPAlert 28 | 29 | extension ProfileController: UITextFieldDelegate { 30 | 31 | func textFieldDidEndEditing(_ textField: UITextField) { 32 | 33 | let nameTextField: UITextField? = { 34 | guard let indexPath = diffableDataSource?.getIndexPath(id: Item.name.id) else { return nil } 35 | guard let cell = tableView.cellForRow(at: indexPath) as? SPDiffableTextFieldTitleTableViewCell else { return nil } 36 | return cell.textField 37 | }() 38 | 39 | switch textField { 40 | case nameTextField: 41 | let text = textField.text ?? .empty 42 | self.profileModel.setName(text, completion: { error in 43 | if let error = error { 44 | SPAlert.present(message: error.localizedDescription, haptic: .error) 45 | } 46 | nameTextField?.text = self.profileModel.name 47 | }) 48 | default: 49 | break 50 | } 51 | } 52 | 53 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 54 | textField.resignFirstResponder() 55 | return true 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Interface/Profile/ProfileController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import SparrowKit 24 | import NativeUIKit 25 | import SPDiffable 26 | import SPSafeSymbols 27 | import SPAlert 28 | 29 | class ProfileController: NativeProfileController { 30 | 31 | internal var profileModel: ProfileModel 32 | 33 | // MARK: - Init 34 | 35 | init() { 36 | if let currentProfile = ProfileModel.currentProfile { 37 | self.profileModel = currentProfile 38 | } else { 39 | fatalError("Profile scene shoud show only if profile available") 40 | } 41 | super.init() 42 | } 43 | 44 | required init?(coder: NSCoder) { 45 | fatalError("init(coder:) has not been implemented") 46 | } 47 | 48 | deinit { 49 | NotificationCenter.default.removeObserver(self) 50 | } 51 | 52 | // MARK: - Lifecycle 53 | 54 | override func viewDidLoad() { 55 | super.viewDidLoad() 56 | 57 | tableView.register(DeviceTableCell.self) 58 | tableView.register(NativeLeftButtonTableViewCell.self) 59 | 60 | configureDiffable(sections: content, cellProviders: [.button] + [ 61 | .init(clouser: { tableView, indexPath, item in 62 | guard let deviceModel = (item as? SPDiffableWrapperItem)?.model as? ProfileDeviceModel else { return nil } 63 | let cell = self.tableView.dequeueReusableCell(withClass: DeviceTableCell.self, for: indexPath) 64 | cell.setDevice(deviceModel) 65 | return cell 66 | }) 67 | ] + SPDiffableTableDataSource.CellProvider.default) 68 | 69 | NotificationCenter.default.addObserver(forName: SPProfiling.didChangedAuthState, object: nil, queue: nil) { [weak self] _ in 70 | guard let self = self else { return } 71 | if (ProfileModel.currentProfile?.id != self.profileModel.id) { 72 | self.dismissAnimated() 73 | } 74 | } 75 | 76 | NotificationCenter.default.addObserver(forName: SPProfiling.didReloadedProfile, object: nil, queue: nil) { [weak self] _ in 77 | guard let self = self else { return } 78 | if let profileModel = ProfileModel.currentProfile { 79 | self.profileModel = profileModel 80 | self.diffableDataSource?.set(self.content, animated: true) 81 | } 82 | } 83 | 84 | configureHeader() 85 | dismissKeyboardWhenTappedAround() 86 | } 87 | 88 | // MARK: - Diffable 89 | 90 | internal enum Section: String, CaseIterable { 91 | 92 | case pulibc_info 93 | case devices 94 | case sign_out 95 | case delete_account 96 | 97 | var id: String { rawValue } 98 | } 99 | 100 | internal enum Item: String, CaseIterable { 101 | 102 | case name 103 | case email 104 | case sign_out 105 | case delete_account 106 | 107 | var id: String { rawValue } 108 | } 109 | 110 | private var content: [SPDiffableSection] { 111 | return [ 112 | .init( 113 | id: Section.pulibc_info.id, 114 | header: SPDiffableTextHeaderFooter(text: Texts.Profile.public_data_header), 115 | footer: SPDiffableTextHeaderFooter(text: Texts.Profile.public_data_footer), 116 | items: [ 117 | SPDiffableTableRowTextFieldTitle( 118 | id: Item.name.id, 119 | icon: nil, 120 | title: Texts.Profile.name_title, 121 | text: profileModel.name, 122 | placeholder: Texts.Profile.placeholder_name, 123 | autocorrectionType: .no, 124 | keyboardType: .default, 125 | autocapitalizationType: .words, 126 | clearButtonMode: .never, 127 | delegate: self 128 | ), 129 | SPDiffableTableRowTextFieldTitle( 130 | id: Item.email.id, 131 | icon: nil, 132 | title: Texts.Profile.email_title, 133 | text: profileModel.email, 134 | placeholder: .empty, 135 | autocorrectionType: .no, 136 | keyboardType: .emailAddress, 137 | autocapitalizationType: .words, 138 | clearButtonMode: .never, 139 | delegate: nil, 140 | editable: false 141 | ) 142 | ] 143 | ), 144 | .init( 145 | id: Section.devices.id, 146 | header: SPDiffableTextHeaderFooter(text: Texts.Profile.Devices.header), 147 | footer: SPDiffableTextHeaderFooter(text: Texts.Profile.Devices.footer), 148 | items: profileModel.devices.prefix(3).map({ SPDiffableWrapperItem(id: $0.id, model: $0) }) + [ 149 | NativeDiffableLeftButton( 150 | text: Texts.Profile.Devices.manage_devices, 151 | textColor: .tint, 152 | detail: "\(profileModel.devices.count) Devices", 153 | detailColor: .secondaryLabel, 154 | icon: nil, 155 | accessoryType: .disclosureIndicator, 156 | higlightStyle: .content, 157 | action: { item, indexPath in 158 | guard let navigationController = self.navigationController else { return } 159 | navigationController.pushViewController(DevicesController()) 160 | }) 161 | ] 162 | ), 163 | .init( 164 | id: Section.sign_out.id, 165 | header: SPDiffableTextHeaderFooter(text: Texts.Profile.Actions.SignOut.title), 166 | footer: SPDiffableTextHeaderFooter(text: Texts.Profile.Actions.SignOut.description), 167 | items: [ 168 | NativeDiffableLeftButton( 169 | id: Item.sign_out.id, 170 | text: Texts.Profile.Actions.SignOut.title, 171 | icon: .init(.xmark.squareFill), 172 | action: { [weak self] _, indexPath in 173 | guard let self = self else { return } 174 | let sourceView = self.tableView.cellForRow(at: indexPath) ?? UIView() 175 | let cell = self.tableView.cellForRow(at: indexPath) as? SPTableViewCell 176 | cell?.setLoading(true) 177 | 178 | UIAlertController.confirm( 179 | title: Texts.Profile.Actions.SignOut.Confirm.title, 180 | description: Texts.Profile.Actions.SignOut.Confirm.description, 181 | actionTitle: Texts.Profile.Actions.SignOut.title, 182 | cancelTitle: Texts.cancel, 183 | desctructive: true, 184 | completion: { [weak self] confirmed in 185 | guard let self = self else { return } 186 | if confirmed { 187 | ProfileModel.currentProfile?.signOut { error in 188 | if let error = error { 189 | SPAlert.present(message: error.localizedDescription, haptic: .error) 190 | } else { 191 | self.dismissAnimated() 192 | } 193 | cell?.setLoading(false) 194 | } 195 | } else { 196 | cell?.setLoading(false) 197 | } 198 | }, 199 | sourceView: sourceView, 200 | on: self 201 | ) 202 | } 203 | ) 204 | ] 205 | ), 206 | .init( 207 | id: Section.delete_account.id, 208 | header: SPDiffableTextHeaderFooter(text: Texts.Profile.Actions.Delete.header), 209 | footer: SPDiffableTextHeaderFooter(text: Texts.Profile.Actions.Delete.description), 210 | items: [ 211 | NativeDiffableLeftButton( 212 | id: Item.delete_account.id, 213 | text: Texts.Profile.Actions.Delete.title, 214 | textColor: .destructiveColor, 215 | icon: .init(.trash.fill).withTintColor(.destructiveColor, renderingMode: .alwaysOriginal), 216 | action: { [weak self] _, indexPath in 217 | guard let self = self else { return } 218 | let sourceView = self.tableView.cellForRow(at: indexPath) ?? UIView() 219 | let cell = self.tableView.cellForRow(at: indexPath) as? SPTableViewCell 220 | cell?.setLoading(true) 221 | UIAlertController.confirmDouble( 222 | title: Texts.Profile.Actions.Delete.Confirm.title, 223 | description: Texts.Profile.Actions.Delete.Confirm.description, 224 | actionTitle: Texts.Profile.Actions.Delete.title, 225 | cancelTitle: Texts.cancel, 226 | desctructive: true, 227 | completion: { [weak self] confirmed in 228 | guard let self = self else { return } 229 | if confirmed { 230 | self.profileModel.delete(on: self) { error in 231 | cell?.setLoading(false) 232 | if let error = error { 233 | SPAlert.present(message: error.localizedDescription, haptic: .error) 234 | } 235 | } 236 | } else { 237 | cell?.setLoading(false) 238 | } 239 | }, 240 | sourceView: sourceView, 241 | on: self 242 | ) 243 | } 244 | ) 245 | ] 246 | ) 247 | ] 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Interface/Profile/Table/CellProvider+Profile.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import SPDiffable 24 | 25 | extension SPDiffableTableDataSource.CellProvider { 26 | 27 | public static var profile: SPDiffableTableDataSource.CellProvider { 28 | return SPDiffableTableDataSource.CellProvider() { (tableView, indexPath, item) -> UITableViewCell? in 29 | guard let item = item as? DiffableProfileItem else { return nil } 30 | let cell = tableView.dequeueReusableCell(withClass: ProfileTableViewCell.self, for: indexPath) 31 | cell.profileLabelsView.descriptionLabel.text = item.cellProfileSubtitle 32 | cell.authLabelsView.descriptionLabel.text = item.cellAuthSubtitle 33 | return cell 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Interface/Profile/Table/DiffableProfileItem.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import SparrowKit 24 | import SPDiffable 25 | import NativeUIKit 26 | 27 | open class DiffableProfileItem: SPDiffableActionableItem { 28 | 29 | public static let id: String = "spprofiling-profile-item" 30 | 31 | var cellAuthSubtitle: String 32 | var cellProfileSubtitle: String 33 | 34 | public init( 35 | authTitle: String, 36 | authDescription: String, 37 | cellAuthSubtitle: String, 38 | cellProfileSubtitle: String, 39 | features: [NativeOnboardingFeatureView.FeatureModel], 40 | presentOn controller: UIViewController 41 | ) { 42 | self.cellAuthSubtitle = cellAuthSubtitle 43 | self.cellProfileSubtitle = cellProfileSubtitle 44 | super.init(id: Self.id, action: { item, indexPath in 45 | if ProfileModel.isAnonymous ?? true { 46 | ProfileModel.showAuth(title: authTitle, description: authDescription, features: features, on: controller) 47 | } else { 48 | ProfileModel.showCurrentProfile(on: controller) 49 | } 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Interface/Profile/Table/ProfileTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import NativeUIKit 24 | import SparrowKit 25 | 26 | open class ProfileTableViewCell: SPTableViewCell { 27 | 28 | // MARK: - Views 29 | 30 | public let profileLabelsView = ProfileLabelsView() 31 | 32 | public let authLabelsView = AuthLabelsView() 33 | 34 | public let avatarView = NativeAvatarView().do { 35 | $0.isEditable = false 36 | $0.placeholderImage = NativeAvatarView.generatePlaceholderImage(fontSize: 46, fontWeight: .medium) 37 | $0.avatarAppearance = .placeholder 38 | } 39 | 40 | // MARK: - Init 41 | 42 | open override func commonInit() { 43 | super.commonInit() 44 | higlightStyle = .content 45 | contentView.addSubviews(avatarView, profileLabelsView, authLabelsView) 46 | accessoryType = .disclosureIndicator 47 | updateAppearance() 48 | configureObservers() 49 | } 50 | 51 | open override func prepareForReuse() { 52 | super.prepareForReuse() 53 | configureObservers() 54 | updateAppearance() 55 | } 56 | 57 | deinit { 58 | NotificationCenter.default.removeObserver(self) 59 | } 60 | 61 | // MARK: - Layout 62 | 63 | open override func layoutSubviews() { 64 | super.layoutSubviews() 65 | avatarView.sizeToFit() 66 | avatarView.setXToSuperviewLeftMargin() 67 | avatarView.frame.origin.y = contentView.layoutMargins.top 68 | 69 | let visibleLabelsView = profileLabelsView.isHidden ? authLabelsView : profileLabelsView 70 | let avatarRightSpace: CGFloat = NativeLayout.Spaces.default 71 | let labelsWidth = contentView.layoutWidth - avatarView.frame.width - avatarRightSpace 72 | visibleLabelsView.frame.setWidth(labelsWidth) 73 | visibleLabelsView.sizeToFit() 74 | visibleLabelsView.frame.origin.x = avatarView.frame.maxX + avatarRightSpace 75 | 76 | if (avatarView.frame.origin.y + visibleLabelsView.frame.height) > avatarView.frame.maxY { 77 | visibleLabelsView.frame.origin.y = contentView.layoutMargins.top 78 | } else { 79 | visibleLabelsView.frame.origin.y = contentView.layoutMargins.top + (contentView.layoutHeight - visibleLabelsView.frame.height) / 2 80 | } 81 | } 82 | 83 | open override func sizeThatFits(_ size: CGSize) -> CGSize { 84 | layoutSubviews() 85 | let visibleLabelsView = profileLabelsView.isHidden ? authLabelsView : profileLabelsView 86 | return .init(width: size.width, height: max(avatarView.frame.maxY, visibleLabelsView.frame.maxY) + contentView.layoutMargins.bottom) 87 | } 88 | 89 | // MARK: - Internal 90 | 91 | internal func configureObservers() { 92 | // Clean 93 | NotificationCenter.default.removeObserver(self) 94 | 95 | // Configure New 96 | NotificationCenter.default.addObserver(forName: SPProfiling.didChangedAuthState, object: nil, queue: nil) { [weak self] _ in 97 | guard let self = self else { return } 98 | self.updateAppearance() 99 | } 100 | 101 | NotificationCenter.default.addObserver(forName: SPProfiling.didReloadedProfile, object: nil, queue: nil) { [weak self] _ in 102 | guard let self = self else { return } 103 | self.updateAppearance() 104 | } 105 | } 106 | 107 | internal func updateAppearance() { 108 | let profileModel = ProfileModel.currentProfile 109 | authLabelsView.titleLabel.text = Texts.Auth.sign_in 110 | profileLabelsView.titleLabel.text = profileModel?.name ?? profileModel?.email ?? Texts.Profile.placeholder_name 111 | 112 | if ProfileModel.isAnonymous ?? true { 113 | avatarView.avatarAppearance = .placeholder 114 | authLabelsView.isHidden = false 115 | profileLabelsView.isHidden = true 116 | } else { 117 | guard let profileModel = ProfileModel.currentProfile else { return } 118 | avatarView.setAvatar(of: profileModel) 119 | authLabelsView.isHidden = true 120 | profileLabelsView.isHidden = false 121 | } 122 | 123 | layoutSubviews() 124 | } 125 | 126 | // MARK: - Views 127 | 128 | public class ProfileLabelsView: SPView { 129 | 130 | public let titleLabel = SPLabel().do { 131 | $0.numberOfLines = 1 132 | $0.font = UIFont.preferredFont(forTextStyle: .title2, weight: .semibold) 133 | $0.textColor = .label 134 | } 135 | 136 | public let descriptionLabel = SPLabel().do { 137 | $0.numberOfLines = 1 138 | $0.font = UIFont.preferredFont(forTextStyle: .footnote, weight: .regular) 139 | $0.textColor = .secondaryLabel 140 | } 141 | 142 | public override func commonInit() { 143 | super.commonInit() 144 | layoutMargins = .zero 145 | addSubview(titleLabel) 146 | addSubview(descriptionLabel) 147 | } 148 | 149 | public override func layoutSubviews() { 150 | super.layoutSubviews() 151 | titleLabel.layoutDynamicHeight(x: .zero, y: .zero, width: frame.width) 152 | descriptionLabel.layoutDynamicHeight(x: .zero, y: titleLabel.frame.maxY + 2, width: frame.width) 153 | } 154 | 155 | public override func sizeThatFits(_ size: CGSize) -> CGSize { 156 | layoutSubviews() 157 | return .init(width: size.width, height: descriptionLabel.frame.maxY) 158 | } 159 | } 160 | 161 | public class AuthLabelsView: ProfileLabelsView { 162 | 163 | public override func commonInit() { 164 | super.commonInit() 165 | titleLabel.textColor = .tint 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Models/AuthWay.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum AuthWay { 4 | 5 | case onlyAuthed 6 | case anonymouslyAllowed 7 | } 8 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Models/Errors/AuthError.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | import SPFirebaseAuth 24 | 25 | public enum AuthError: LocalizedError { 26 | 27 | case canceled 28 | case cantPresent 29 | case cantPrepareRequiredData 30 | 31 | public var errorDescription: String? { 32 | switch self { 33 | case .canceled: return Texts.Error.Auth.canceled 34 | case .cantPresent: return Texts.Error.Auth.cant_present 35 | case .cantPrepareRequiredData: return Texts.Error.Auth.cant_prepare_required_data 36 | } 37 | } 38 | 39 | static func convert(_ error: SPFirebaseAuthError) -> AuthError { 40 | switch error { 41 | case .canceled: return .canceled 42 | case .cantPresent: return .cantPresent 43 | case .faild: return .cantPrepareRequiredData 44 | } 45 | } 46 | } 47 | 48 | 49 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Models/Errors/DeleteAvatarError.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | 24 | public enum DeleteAvatarError: LocalizedError { 25 | 26 | case notEnoughPermissions 27 | case notReachable 28 | 29 | public var errorDescription: String? { 30 | switch self { 31 | case .notEnoughPermissions: return "notEnoughPermissions" 32 | case .notReachable: return "case notReachable" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Models/Errors/DeleteProfileError.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | import SPFirebaseFirestore 24 | 25 | public enum DeleteProfileError: LocalizedError { 26 | 27 | case notEnoughPermissions 28 | case notReachable 29 | case unknown 30 | 31 | public var errorDescription: String? { 32 | switch self { 33 | case .notEnoughPermissions: return "notEnoughPermissions" 34 | case .notReachable: return Texts.Error.not_reachable 35 | case .unknown: return Texts.Error.unknown 36 | } 37 | } 38 | 39 | public static func convert(_ error: Error) -> EditProfileError { 40 | let error = SPFirebaseFirestoreError.get(by: error) 41 | switch error { 42 | case .OK: return .unknown 43 | case .cancelled: return .notReachable 44 | case .unknown: return .unknown 45 | case .invalidArgument: return .unknown 46 | case .deadlineExceeded: return .unknown 47 | case .notFound: return .unknown 48 | case .alreadyExists: return .unknown 49 | case .permissionDenied: return .notEnoughPermissions 50 | case .resourceExhausted: return .unknown 51 | case .failedPrecondition: return .unknown 52 | case .aborted: return .notReachable 53 | case .outOfRange: return .unknown 54 | case .unimplemented: return .unknown 55 | case .internal: return .unknown 56 | case .unavailable: return .notReachable 57 | case .dataLoss: return .notReachable 58 | case .unauthenticated: return .notEnoughPermissions 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Models/Errors/EditProfileError.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | import SPFirebaseFirestore 24 | 25 | public enum EditProfileError: LocalizedError { 26 | 27 | case notEnoughPermissions 28 | case notReachable 29 | case unknown 30 | 31 | public var errorDescription: String? { 32 | switch self { 33 | case .notEnoughPermissions: return Texts.Error.not_enough_permissions 34 | case .notReachable: return Texts.Error.not_reachable 35 | case .unknown: return Texts.Error.unknown 36 | } 37 | } 38 | 39 | public static func convert(_ error: Error) -> EditProfileError { 40 | let error = SPFirebaseFirestoreError.get(by: error) 41 | switch error { 42 | case .OK: return .unknown 43 | case .cancelled: return .notReachable 44 | case .unknown: return .unknown 45 | case .invalidArgument: return .unknown 46 | case .deadlineExceeded: return .unknown 47 | case .notFound: return .unknown 48 | case .alreadyExists: return .unknown 49 | case .permissionDenied: return .notEnoughPermissions 50 | case .resourceExhausted: return .unknown 51 | case .failedPrecondition: return .unknown 52 | case .aborted: return .notReachable 53 | case .outOfRange: return .unknown 54 | case .unimplemented: return .unknown 55 | case .internal: return .unknown 56 | case .unavailable: return .notReachable 57 | case .dataLoss: return .notReachable 58 | case .unauthenticated: return .notEnoughPermissions 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Models/Errors/GetAvatarError.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | 24 | public enum GetAvatarError: LocalizedError { 25 | 26 | case notEnoughPermissions 27 | case notReachable 28 | 29 | public var errorDescription: String? { 30 | switch self { 31 | case .notEnoughPermissions: return "notEnoughPermissions" 32 | case .notReachable: return "notReachable" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Models/Errors/GetProfileError.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | import SPFirebaseFirestore 24 | 25 | public enum GetProfileError: LocalizedError { 26 | 27 | case notFound 28 | case notEnoughPermissions 29 | case notReachable 30 | case unknown 31 | 32 | public var errorDescription: String? { 33 | switch self { 34 | case .notFound: return Texts.Error.not_found 35 | case .notEnoughPermissions: return Texts.Error.not_enough_permissions 36 | case .notReachable: return Texts.Error.not_reachable 37 | case .unknown: return Texts.Error.unknown 38 | } 39 | } 40 | 41 | public static func convert(_ error: Error) -> GetProfileError { 42 | let error = SPFirebaseFirestoreError.get(by: error) 43 | switch error { 44 | case .OK: return .unknown 45 | case .cancelled: return .notReachable 46 | case .unknown: return .unknown 47 | case .invalidArgument: return .unknown 48 | case .deadlineExceeded: return .unknown 49 | case .notFound: return .notFound 50 | case .alreadyExists: return .unknown 51 | case .permissionDenied: return .notEnoughPermissions 52 | case .resourceExhausted: return .unknown 53 | case .failedPrecondition: return .unknown 54 | case .aborted: return .notReachable 55 | case .outOfRange: return .unknown 56 | case .unimplemented: return .unknown 57 | case .internal: return .unknown 58 | case .unavailable: return .notReachable 59 | case .dataLoss: return .notReachable 60 | case .unauthenticated: return .notEnoughPermissions 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Models/Errors/SetAvatarError.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | 24 | public enum SetAvatarError: LocalizedError { 25 | 26 | case notEnoughPermissions 27 | case invalidData 28 | case notReachable 29 | 30 | public var errorDescription: String? { 31 | switch self { 32 | case .notEnoughPermissions: return "notEnoughPermissions" 33 | case .invalidData: return "invalidData" 34 | case .notReachable: return "notReachable" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Models/Middleware/ProfileMiddlewareError.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | 24 | public enum ProfileMiddlewareError: LocalizedError { 25 | 26 | case nameShort 27 | case nameLong 28 | case emptyName 29 | case invalidEmail 30 | case avatarBigWidth 31 | case avatarBigSize 32 | 33 | public var errorDescription: String? { 34 | switch self { 35 | case .nameShort: return Texts.Error.Profile.name_short 36 | case .nameLong: return Texts.Error.Profile.name_long 37 | case .emptyName: return Texts.Error.Profile.empty_name 38 | case .invalidEmail: return Texts.Error.Profile.empty_name 39 | case .avatarBigWidth: return Texts.Error.Profile.big_avatar_width 40 | case .avatarBigSize: return Texts.Error.Profile.big_avatar_size 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Models/Middleware/ProfileModel+Middleware.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | 24 | extension ProfileModel { 25 | 26 | public static func middleware(_ profileModel: ProfileModel) -> Error? { 27 | if let name = profileModel.name, let error = middlewareProfileName(name) { return error } 28 | if let email = profileModel.email, let error = middlewareProfileEmail(email) { return error } 29 | return nil 30 | } 31 | 32 | // MARK: - Name 33 | 34 | public static func cleanProfileName(_ name: String) -> String { 35 | return name.trim 36 | } 37 | 38 | public static func validProfileName(_ name: String) -> Bool { 39 | return middlewareProfileName(name) == nil 40 | } 41 | 42 | public static func middlewareProfileName(_ name: String) -> Error? { 43 | if name.isEmptyContent { return ProfileMiddlewareError.emptyName } 44 | if name.count < Self.minimum_profile_name_length { 45 | return ProfileMiddlewareError.nameShort 46 | } 47 | if name.count > Self.maximum_profile_name_length { 48 | return ProfileMiddlewareError.nameLong 49 | } 50 | return nil 51 | } 52 | 53 | // MARK: - Email 54 | 55 | public static func validProfileEmail(_ string: String) -> Bool { 56 | return middlewareProfileEmail(string) == nil 57 | } 58 | 59 | public static func middlewareProfileEmail(_ email: String) -> Error? { 60 | guard email.isValidEmail else { 61 | return ProfileMiddlewareError.invalidEmail 62 | } 63 | return nil 64 | } 65 | 66 | // MARK: - Avatar 67 | 68 | public static func cleanAvatar(image: UIImage) -> UIImage? { 69 | let maximmumWidth = CGFloat(Self.avatar_maximum_width) 70 | let comressFactor = CGFloat(Self.avatar_compress_factor) 71 | var compressedImage = image.compresse(quality: comressFactor) ?? image 72 | if compressedImage.size.width > maximmumWidth { 73 | compressedImage = compressedImage.resize(newWidth: maximmumWidth) 74 | } 75 | return compressedImage 76 | } 77 | 78 | public static func middlewareAvatar(_ image: UIImage) -> Error? { 79 | if image.size.width > Self.avatar_maximum_width { return ProfileMiddlewareError.avatarBigWidth } 80 | if image.bytesSize > Self.avatar_maximum_bytes { return ProfileMiddlewareError.avatarBigSize } 81 | return nil 82 | } 83 | 84 | // MARK: - Constants 85 | 86 | public static var minimum_profile_name_length: Int { 2 } 87 | public static var maximum_profile_name_length: Int { 50 } 88 | public static var avatar_maximum_width: CGFloat { 100 } 89 | public static var avatar_compress_factor: Float { 0.5 } 90 | public static var avatar_maximum_bytes: Int64 { 10 * 1024 * 1024 } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Models/ProfileAction.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | 24 | public enum ProfileAction { 25 | 26 | case getProfile 27 | case editProfile(_ profileModel: ProfileModel) 28 | case deleteProfile(_ profileModel: ProfileModel) 29 | case getProfilveAvatar 30 | case setProfilveAvatar(_ profileModel: ProfileModel) 31 | } 32 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Models/ProfileDeviceModel.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | 24 | public struct ProfileDeviceModel: Equatable { 25 | 26 | public let id: String 27 | public let name: String 28 | public let type: DeviceType 29 | public let fcmToken: String? 30 | public let languageCode: String 31 | public let addedDate: Date 32 | 33 | public init(id: String, name: String, type: DeviceType, fcmToken: String?, languageCode: String, addedDate: Date) { 34 | self.id = id 35 | self.name = name 36 | self.type = type 37 | self.fcmToken = fcmToken 38 | self.languageCode = languageCode 39 | self.addedDate = addedDate 40 | } 41 | 42 | // MARK: - Models 43 | 44 | public enum DeviceType: String { 45 | 46 | case phone 47 | case pad 48 | case desktop 49 | 50 | public var id: String { rawValue } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Models/ProfileModel.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | 24 | public struct ProfileModel: Equatable { 25 | 26 | public let id: String 27 | public let name: String? 28 | public let email: String? 29 | public let createdDate: Date? 30 | public let devices: [ProfileDeviceModel] 31 | 32 | public init(id: String, name: String? = nil, email: String? = nil, createdDate: Date? = nil, devices: [ProfileDeviceModel]) { 33 | self.id = id 34 | self.name = name 35 | self.email = email 36 | self.createdDate = createdDate 37 | self.devices = devices 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Notifications.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | 24 | extension SPProfiling { 25 | 26 | public static var didChangedAuthState = Notification.Name("didChangedAuthState") 27 | public static var didReloadedProfile = Notification.Name("didReloadedProfile") 28 | } 29 | -------------------------------------------------------------------------------- /Sources/SPProfiling/ProfileModelExtension.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import SparrowKit 24 | import NativeUIKit 25 | import SPFirebaseFirestore 26 | 27 | extension ProfileModel { 28 | 29 | public static var isAuthed: Bool { Auth.isAuthed } 30 | public static var isAnonymous: Bool? { Auth.isAnonymous } 31 | public static var currentProfile: ProfileModel? { SPProfiling.Profile.currentProfile } 32 | 33 | public static func getProfile(userID: String, source: SPFirebaseFirestoreSource, completion: @escaping (ProfileModel?, Error?)->Void) { 34 | SPProfiling.Profile.getProfile(userID: userID, source: source, completion: completion) 35 | } 36 | 37 | public static func getProfile(email: String, source: SPFirebaseFirestoreSource, completion: @escaping (ProfileModel?, Error?)->Void) { 38 | SPProfiling.Profile.getProfile(email: email, source: source, completion: completion) 39 | } 40 | 41 | // MARK: - Auth 42 | 43 | @available(iOS 13.0, *) 44 | public static func signInApple(on controller: UIViewController, completion: @escaping (AuthError?)->Void) { 45 | SPProfiling.signInApple(on: controller, completion: completion) 46 | } 47 | 48 | public static func signInAnonymously(completion: @escaping (AuthError?)->Void) { 49 | SPProfiling.signInAnonymously(completion: completion) 50 | } 51 | 52 | public func signOut(completion: @escaping (AuthError?)->Void) { 53 | SPProfiling.signOut(completion: completion) 54 | } 55 | 56 | public func delete(on controller: UIViewController, completion: @escaping (Error?)->Void) { 57 | SPProfiling.deleteProfile(on: controller, completion: completion) 58 | } 59 | 60 | // MARK: - Data 61 | 62 | public func setName(_ value: String, completion: ((Error?)->Void)?) { 63 | SPProfiling.Profile.setProfileName(value, completion: completion) 64 | } 65 | 66 | public func getAvatarURL(completion: @escaping (URL?, Error?)->Void) { 67 | SPProfiling.Profile.getAvatarURL(of: self, completion: completion) 68 | } 69 | 70 | public func setAvatar(_ image: UIImage, completion: ((Error?)->Void)?) { 71 | SPProfiling.Profile.setAvatar(image, completion: completion) 72 | } 73 | 74 | public func deleteAvatar(completion: ((DeleteAvatarError?)->Void)?) { 75 | SPProfiling.Profile.deleteAvatar(completion: completion) 76 | } 77 | 78 | // MARK: - Interface 79 | 80 | public static func showCurrentProfile(on viewController: UIViewController) { 81 | guard currentProfile != nil else { return } 82 | 83 | let controller = ProfileController() 84 | let navigationController = controller.wrapToNavigationController(prefersLargeTitles: false) 85 | controller.navigationItem.rightBarButtonItem = controller.closeBarButtonItem 86 | 87 | let horizontalMargin: CGFloat = NativeLayout.Spaces.Margins.modal_screen_horizontal 88 | controller.modalPresentationStyle = .formSheet 89 | controller.preferredContentSize = .init(width: 540, height: 620) 90 | controller.view.layoutMargins.left = horizontalMargin 91 | controller.view.layoutMargins.right = horizontalMargin 92 | 93 | navigationController.inheritLayoutMarginsForNavigationBar = true 94 | navigationController.inheritLayoutMarginsForСhilds = true 95 | navigationController.viewDidLayoutSubviews() 96 | 97 | viewController.present(navigationController) 98 | } 99 | 100 | public static func showAuth( 101 | title: String, 102 | description: String, 103 | features: [NativeOnboardingFeatureView.FeatureModel], 104 | on presentingController: UIViewController 105 | ) { 106 | let authController = AuthController(title: title, description: description, completion: { authController in 107 | guard ProfileModel.isAuthed else { return } 108 | authController.dismiss(animated: true) { 109 | if !(ProfileModel.isAnonymous ?? true) { 110 | ProfileModel.showCurrentProfile(on: presentingController) 111 | } 112 | } 113 | }) 114 | authController.setFeatures(features) 115 | let navigationController = NativeNavigationController(rootViewController: authController) 116 | authController.navigationItem.rightBarButtonItem = authController.closeBarButtonItem 117 | 118 | let horizontalMargin: CGFloat = NativeLayout.Spaces.Margins.modal_screen_horizontal 119 | authController.modalPresentationStyle = .formSheet 120 | authController.preferredContentSize = .init(width: 540, height: 620) 121 | authController.view.layoutMargins.left = horizontalMargin 122 | authController.view.layoutMargins.right = horizontalMargin 123 | 124 | navigationController.inheritLayoutMarginsForNavigationBar = true 125 | navigationController.inheritLayoutMarginsForСhilds = true 126 | navigationController.viewDidLayoutSubviews() 127 | 128 | presentingController.present(navigationController) 129 | } 130 | 131 | 132 | // MARK: - Middleware 133 | 134 | public func canExecute(_ action: ProfileAction) -> Bool { 135 | let error = middleware(action) 136 | return error == nil 137 | } 138 | 139 | public func middleware(_ action: ProfileAction) -> Error? { 140 | switch action { 141 | case .getProfile: 142 | return nil 143 | case .getProfilveAvatar: 144 | return nil 145 | case .editProfile(let profileModel): 146 | return self.id == profileModel.id ? nil : EditProfileError.notEnoughPermissions 147 | case .deleteProfile(let profileModel): 148 | return self.id == profileModel.id ? nil : EditProfileError.notEnoughPermissions 149 | case .setProfilveAvatar(let profileModel): 150 | return self.id == profileModel.id ? nil : SetAvatarError.notEnoughPermissions 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Resources/Localization/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "auth continue anonymously" = "Continue Anonymously"; 3 | 4 | /* No comment provided by engineer. */ 5 | "auth description" = "Log in to access the benefits."; 6 | 7 | /* No comment provided by engineer. */ 8 | "auth footer description" = "Profile can be abandoned and deleted at any time. You can change your name and avatar after logging in."; 9 | 10 | /* No comment provided by engineer. */ 11 | "auth sign in" = "Sign In"; 12 | 13 | /* No comment provided by engineer. */ 14 | "cancel" = "Cancel"; 15 | 16 | /* No comment provided by engineer. */ 17 | "error auth canceled" = "Authentication canceled"; 18 | 19 | /* No comment provided by engineer. */ 20 | "error auth cant prepare required data" = "Failed to prepare the necessary data"; 21 | 22 | /* No comment provided by engineer. */ 23 | "error auth cant prepare required data" = "Failed to prepare the required data"; 24 | 25 | /* No comment provided by engineer. */ 26 | "error auth cant present" = "Failed to show authentication screen"; 27 | 28 | /* No comment provided by engineer. */ 29 | "error not enough permissions" = "Not enough permissions"; 30 | 31 | /* No comment provided by engineer. */ 32 | "error not found" = "Not Found"; 33 | 34 | /* No comment provided by engineer. */ 35 | "error not reachable" = "Not Reachable"; 36 | 37 | /* No comment provided by engineer. */ 38 | "error profile big avatar size" = "The size of the avatar is too large."; 39 | 40 | /* No comment provided by engineer. */ 41 | "error profile big avatar width" = "The width of the avatar is too wide."; 42 | 43 | /* No comment provided by engineer. */ 44 | "error profile empty name" = "The profile name cannot be empty."; 45 | 46 | /* No comment provided by engineer. */ 47 | "error profile name long" = "Profile name is too long."; 48 | 49 | /* No comment provided by engineer. */ 50 | "error profile name short" = "Profile name is too short."; 51 | 52 | /* No comment provided by engineer. */ 53 | "error unknown" = "Unknown Error"; 54 | 55 | /* No comment provided by engineer. */ 56 | "profile actions delete confirm description" = "The profile will be permanently deleted with all data. This action cannot be undone."; 57 | 58 | /* No comment provided by engineer. */ 59 | "profile actions delete confirm title" = "Delete Profile?"; 60 | 61 | /* No comment provided by engineer. */ 62 | "profile actions delete description" = "The profile will be deleted along with all data."; 63 | 64 | /* No comment provided by engineer. */ 65 | "profile actions delete header" = "Delete"; 66 | 67 | /* No comment provided by engineer. */ 68 | "profile actions delete title" = "Delete Profile"; 69 | 70 | /* No comment provided by engineer. */ 71 | "profile actions email copied" = "Email Copied"; 72 | 73 | /* No comment provided by engineer. */ 74 | "profile actions rename description" = "The name will be visible to everybody."; 75 | 76 | /* No comment provided by engineer. */ 77 | "profile actions rename title" = "Enter New Name"; 78 | 79 | /* No comment provided by engineer. */ 80 | "profile actions sign out confirm description" = "All data will be erased from this device. You will be able to restore them by logging in to your account."; 81 | 82 | /* No comment provided by engineer. */ 83 | "profile actions sign out confirm title" = "Sign Out?"; 84 | 85 | /* No comment provided by engineer. */ 86 | "profile actions sign out description" = "When you exit the profile, all data will be deleted from the device."; 87 | 88 | /* No comment provided by engineer. */ 89 | "profile actions sign out title" = "Sign Out"; 90 | 91 | /* No comment provided by engineer. */ 92 | "profile avatar actions description" = "Select where you want to install the photo from."; 93 | 94 | /* No comment provided by engineer. */ 95 | "profile avatar delete action title" = "Delete Avatar"; 96 | 97 | /* No comment provided by engineer. */ 98 | "profile avatar open camera" = "Camera"; 99 | 100 | /* No comment provided by engineer. */ 101 | "profile avatar open photo library" = "Photo Library"; 102 | 103 | /* No comment provided by engineer. */ 104 | "profile devices added date" = "Added on %@"; 105 | 106 | /* No comment provided by engineer. */ 107 | "profile devices footer" = "You can disable access to a profile by deleting it."; 108 | 109 | /* No comment provided by engineer. */ 110 | "profile devices header" = "Devices"; 111 | 112 | /* No comment provided by engineer. */ 113 | "profile devices manage devices" = "Manage Devices"; 114 | 115 | /* No comment provided by engineer. */ 116 | "profile devices title" = "Devices"; 117 | 118 | /* No comment provided by engineer. */ 119 | "profile email title" = "Email"; 120 | 121 | /* No comment provided by engineer. */ 122 | "profile name title" = "Name"; 123 | 124 | /* No comment provided by engineer. */ 125 | "profile placeholder name" = "No Name"; 126 | 127 | /* No comment provided by engineer. */ 128 | "profile public data footer" = "The information will be visible to all users."; 129 | 130 | /* No comment provided by engineer. */ 131 | "profile public data header" = "Personal"; 132 | 133 | /* No comment provided by engineer. */ 134 | "profile rename alert description" = "The name will be visible for everybody."; 135 | 136 | /* No comment provided by engineer. */ 137 | "profile rename alert placeholder" = "Name Surname"; 138 | 139 | /* No comment provided by engineer. */ 140 | "profile rename alert title" = "Enter New Name"; 141 | 142 | /* No comment provided by engineer. */ 143 | "save" = "Save"; 144 | 145 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Resources/Localization/ru.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "auth continue anonymously" = "Продолжить анонимно"; 3 | 4 | /* No comment provided by engineer. */ 5 | "auth description" = "Войдите, чтобы получить доступ к преимуществам."; 6 | 7 | /* No comment provided by engineer. */ 8 | "auth footer description" = "Профиль можно покинуть или удалить в любое время."; 9 | 10 | /* No comment provided by engineer. */ 11 | "auth sign in" = "Войти"; 12 | 13 | /* No comment provided by engineer. */ 14 | "cancel" = "Отменить"; 15 | 16 | /* No comment provided by engineer. */ 17 | "error auth canceled" = "Аутентификация отменена"; 18 | 19 | /* No comment provided by engineer. */ 20 | "error auth cant prepare required data" = "Не удалось подготовить необходимые данные"; 21 | 22 | /* No comment provided by engineer. */ 23 | "error auth cant prepare required data" = "Не удалось подготовить запрашиваемые данные"; 24 | 25 | /* No comment provided by engineer. */ 26 | "error auth cant present" = "Не удалось показать экран аутентификации"; 27 | 28 | /* No comment provided by engineer. */ 29 | "error not enough permissions" = "Недостаточно прав"; 30 | 31 | /* No comment provided by engineer. */ 32 | "error not found" = "Не найдено"; 33 | 34 | /* No comment provided by engineer. */ 35 | "error not reachable" = "Недоступно"; 36 | 37 | /* No comment provided by engineer. */ 38 | "error profile big avatar size" = "Размер аватара слишком большой."; 39 | 40 | /* No comment provided by engineer. */ 41 | "error profile big avatar width" = "Аватар слишком широкий."; 42 | 43 | /* No comment provided by engineer. */ 44 | "error profile empty name" = "Имя профиля не может быть пустым."; 45 | 46 | /* No comment provided by engineer. */ 47 | "error profile name long" = "Имя профиля слишком длинное."; 48 | 49 | /* No comment provided by engineer. */ 50 | "error profile name short" = "Имя слишком короткое."; 51 | 52 | /* No comment provided by engineer. */ 53 | "error unknown" = "Неизвестная ошибка"; 54 | 55 | /* No comment provided by engineer. */ 56 | "profile actions delete confirm description" = "Профиль будет безвозвратно удален со всеми данными. Это действие нельзя отменить."; 57 | 58 | /* No comment provided by engineer. */ 59 | "profile actions delete confirm title" = "Удалить профиль?"; 60 | 61 | /* No comment provided by engineer. */ 62 | "profile actions delete description" = "Профиль будет удален вместе со всеми данными."; 63 | 64 | /* No comment provided by engineer. */ 65 | "profile actions delete header" = "Удаление"; 66 | 67 | /* No comment provided by engineer. */ 68 | "profile actions delete title" = "Удалить профиль"; 69 | 70 | /* No comment provided by engineer. */ 71 | "profile actions email copied" = "Почта скопирована"; 72 | 73 | /* No comment provided by engineer. */ 74 | "profile actions rename description" = "Имя будет видно всем."; 75 | 76 | /* No comment provided by engineer. */ 77 | "profile actions rename title" = "Введите новое имя"; 78 | 79 | /* No comment provided by engineer. */ 80 | "profile actions sign out confirm description" = "Все данные будут удалены с этого устройства. Вы сможете восстановить их, войдя в свою профиль заново."; 81 | 82 | /* No comment provided by engineer. */ 83 | "profile actions sign out confirm title" = "Выйти из профиля?"; 84 | 85 | /* No comment provided by engineer. */ 86 | "profile actions sign out description" = "При выходе из профиля все данные будут удалены с устройства."; 87 | 88 | /* No comment provided by engineer. */ 89 | "profile actions sign out title" = "Выйти"; 90 | 91 | /* No comment provided by engineer. */ 92 | "profile avatar actions description" = "Выберите место, откуда хотите установить фотографию."; 93 | 94 | /* No comment provided by engineer. */ 95 | "profile avatar delete action title" = "Удалить"; 96 | 97 | /* No comment provided by engineer. */ 98 | "profile avatar open camera" = "Камера"; 99 | 100 | /* No comment provided by engineer. */ 101 | "profile avatar open photo library" = "Библиотека фото"; 102 | 103 | /* No comment provided by engineer. */ 104 | "profile devices added date" = "Добавлено %@"; 105 | 106 | /* No comment provided by engineer. */ 107 | "profile devices footer" = "Вы можете запретить доступ к профилю, удалив его."; 108 | 109 | /* No comment provided by engineer. */ 110 | "profile devices header" = "Устройства"; 111 | 112 | /* No comment provided by engineer. */ 113 | "profile devices manage devices" = "Управление устройствами"; 114 | 115 | /* No comment provided by engineer. */ 116 | "profile devices title" = "Устройства"; 117 | 118 | /* No comment provided by engineer. */ 119 | "profile email title" = "Email"; 120 | 121 | /* No comment provided by engineer. */ 122 | "profile name title" = "Имя"; 123 | 124 | /* No comment provided by engineer. */ 125 | "profile placeholder name" = "Нет имени"; 126 | 127 | /* No comment provided by engineer. */ 128 | "profile public data footer" = "Информация будет видна всем пользователям."; 129 | 130 | /* No comment provided by engineer. */ 131 | "profile public data header" = "Личная информация"; 132 | 133 | /* No comment provided by engineer. */ 134 | "profile rename alert description" = "Имя профиля будет видно всем."; 135 | 136 | /* No comment provided by engineer. */ 137 | "profile rename alert placeholder" = "Имя Фамилия"; 138 | 139 | /* No comment provided by engineer. */ 140 | "profile rename alert title" = "Введите новое имя"; 141 | 142 | /* No comment provided by engineer. */ 143 | "save" = "Сохранить"; 144 | 145 | -------------------------------------------------------------------------------- /Sources/SPProfiling/SPProfiling.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import SparrowKit 24 | import Firebase 25 | import SPFirebase 26 | import SPFirebaseAuth 27 | 28 | public class SPProfiling { 29 | 30 | public static func configure(_ authWay: AuthWay, firebaseOptions: FirebaseOptions) { 31 | 32 | shared.authWay = authWay 33 | 34 | // Start Firebase 35 | SPFirebase.configure(with: firebaseOptions) 36 | 37 | // Start Services 38 | Auth.configure() 39 | 40 | // Data services can be reset if auth changed. 41 | // So using special func for all data services. 42 | configureDataServices() 43 | 44 | // Other 45 | shared.setObservers() 46 | } 47 | 48 | // MARK: - Shared Actions 49 | 50 | @available(iOS 13.0, *) 51 | static func signInApple(on controller: UIViewController, completion: @escaping (AuthError?)->Void) { 52 | authProcess = true 53 | Auth.signInApple(on: controller, completion: { data, error in 54 | 55 | if let error = error { 56 | completion(error) 57 | return 58 | } 59 | 60 | guard let profileModel = Profile.currentProfile else { fatalError() } 61 | completion(nil) 62 | 63 | // Get actuall data of profile and update if need. 64 | // No need wait when it completed. 65 | Profile.getProfile(userID: profileModel.id, source: .actuallyOnly) { validProfileModel, error in 66 | guard let validProfileModel = validProfileModel else { return } 67 | if let name = data?.name, validProfileModel.name != name { 68 | SPProfiling.Profile.setProfileName(name, completion: nil) 69 | } 70 | if let email = data?.email, validProfileModel.email != email { 71 | SPProfiling.Profile.setProfileEmail(email, completion: nil) 72 | } 73 | if validProfileModel.createdDate == nil { 74 | SPProfiling.Profile.setProfileCreatedDate(Date.current, completion: nil) 75 | } 76 | } 77 | }) 78 | } 79 | 80 | static func signInAnonymously(completion: @escaping (AuthError?)->Void) { 81 | authProcess = true 82 | Auth.signInAnonymously(completion: { error in 83 | if let error = error { 84 | completion(error) 85 | } else { 86 | completion(nil) 87 | } 88 | authProcess = false 89 | }) 90 | } 91 | 92 | static func signOut(completion: @escaping (AuthError?)->Void) { 93 | Auth.signOut { error in 94 | completion(error) 95 | } 96 | } 97 | 98 | static func deleteProfile(on controller: UIViewController, completion: @escaping (Error?)->Void) { 99 | 100 | let deleteAction = { 101 | SPProfiling.Profile.deleteProfile { error in 102 | if let error = error { 103 | completion(error) 104 | } else { 105 | Auth.delete { error in 106 | completion(error) 107 | } 108 | } 109 | } 110 | } 111 | 112 | if Auth.isAnonymous ?? true { 113 | deleteAction() 114 | } else { 115 | Auth.signInAppleForConfirm(on: controller) { error in 116 | if let error = error { 117 | completion(error) 118 | } else { 119 | deleteAction() 120 | } 121 | } 122 | } 123 | } 124 | 125 | // MARK: - Private 126 | 127 | private static func configureDataServices() { 128 | if Auth.isAuthed, let userID = Auth.userID { 129 | Profile.configure(userID: userID) 130 | } else { 131 | Profile.reset() 132 | } 133 | } 134 | 135 | // MARK: - Private 136 | 137 | private func setObservers() { 138 | NotificationCenter.default.addObserver(forName: SPProfiling.didChangedAuthState, object: nil, queue: nil) { notification in 139 | debug("Logic/Notification: Handled notification about changed auth state.") 140 | Self.configureDataServices() 141 | } 142 | } 143 | 144 | // MARK: - Singltone 145 | 146 | private var authWay: AuthWay = .anonymouslyAllowed 147 | private var authProcess: Bool = false 148 | private static let shared = SPProfiling() 149 | private init() {} 150 | 151 | // MARK: Access 152 | 153 | internal static var authProcess: Bool { 154 | get { SPProfiling.shared.authProcess } 155 | set { SPProfiling.shared.authProcess = newValue } 156 | } 157 | 158 | public static var authWay: AuthWay { shared.authWay } 159 | } 160 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Services/Auth.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import SparrowKit 24 | import SPFirebaseAuth 25 | 26 | class Auth { 27 | 28 | // MARK: - Public 29 | 30 | static func configure() { 31 | debug("SPProfiling/Auth configure.") 32 | SPFirebaseAuth.configure(authDidChangedWork: { 33 | NotificationCenter.default.post(name: SPProfiling.didChangedAuthState) 34 | }) 35 | } 36 | 37 | @available(iOS 13.0, *) 38 | static func signInApple(on controller: UIViewController, completion: @escaping (SPFirebaseAuthData?, AuthError?)->Void) { 39 | let oldUserID = userID 40 | debug("SPProfiling/Auth event, start Sign in with Apple. Old user ID is \(oldUserID ?? "nil").") 41 | SPFirebaseAuth.signInApple(on: controller) { data, error in 42 | if let error = error { 43 | completion(data, AuthError.convert(error)) 44 | } else { 45 | completion(data, nil) 46 | } 47 | } 48 | } 49 | 50 | @available(iOS 13.0, *) 51 | static func signInAppleForConfirm(on controller: UIViewController, completion: @escaping (AuthError?) -> Void) { 52 | SPFirebaseAuth.signInApple(on: controller) { data, error in 53 | if let error = error { 54 | completion(AuthError.convert(error)) 55 | } else { 56 | completion(nil) 57 | } 58 | } 59 | } 60 | 61 | static func signInAnonymously(completion: @escaping (AuthError?)->Void) { 62 | SPFirebaseAuth.signInAnonymously(completion: { error in 63 | if let error = error { 64 | completion(AuthError.convert(error)) 65 | } else { 66 | completion(nil) 67 | } 68 | }) 69 | } 70 | 71 | static func signOut(completion: @escaping (AuthError?)->Void) { 72 | SPFirebaseAuth.signOut(completion: { error in 73 | if let error = error { 74 | completion(AuthError.convert(error)) 75 | } else { 76 | completion(nil) 77 | } 78 | }) 79 | } 80 | 81 | static func delete(completion: @escaping (AuthError?)->Void) { 82 | SPFirebaseAuth.delete { error in 83 | if let error = error { 84 | completion(AuthError.convert(error)) 85 | } else { 86 | signInAnonymously() { error in 87 | completion(error) 88 | } 89 | } 90 | } 91 | } 92 | 93 | // MARK: - Data 94 | 95 | static var userID: String? { SPFirebaseAuth.userID } 96 | static var isAnonymous: Bool? { SPFirebaseAuth.isAnonymous } 97 | static var isAuthed: Bool { SPFirebaseAuth.userID != nil } 98 | 99 | // MARK: - Singltone 100 | 101 | private static var shared = Auth() 102 | private init() {} 103 | } 104 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Services/Profile+Actions.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | import SparrowKit 24 | import FirebaseFirestore 25 | import SPFirebaseFirestore 26 | 27 | extension SPProfiling.Profile { 28 | 29 | static func getProfile(userID: String, source: SPFirebaseFirestoreSource, completion: @escaping (ProfileModel?, Error?)->Void) { 30 | guard let currentProfileModel = currentProfile else { 31 | completion(nil, GetProfileError.notEnoughPermissions) 32 | return 33 | } 34 | if let error = currentProfileModel.middleware(.getProfile) { 35 | completion(nil, error) 36 | return 37 | } 38 | debug("SPProfiling/Profile getting profile of userID \(userID).") 39 | Firestore.profile_document(userID: userID).getDocument(source: source.firestore) { documentSnapshot, error in 40 | if let error = error { 41 | if source == .cachedFirst { 42 | // If cached first need to do make request 43 | getProfile(userID: userID, source: .default, completion: completion) 44 | } else { 45 | debug("SPProfiling/Profile can't get profile for \(userID), error: \(error.localizedDescription)") 46 | completion(nil, GetProfileError.convert(error)) 47 | return 48 | } 49 | } 50 | guard let documentSnapshot = documentSnapshot else { return } 51 | if let profileModel = Converter.convertToProfile(documentSnapshot) { 52 | completion(profileModel, nil) 53 | } else { 54 | completion(nil, GetProfileError.unknown) 55 | } 56 | } 57 | } 58 | 59 | static func getProfile(email: String, source: SPFirebaseFirestoreSource, completion: @escaping (ProfileModel?, Error?)->Void) { 60 | guard let currentProfileModel = currentProfile else { 61 | completion(nil, GetProfileError.notEnoughPermissions) 62 | return 63 | } 64 | if let error = currentProfileModel.middleware(.getProfile) { 65 | completion(nil, error) 66 | return 67 | } 68 | debug("SPProfiling/Profile getting profile of email \(email).") 69 | Firestore.profiles_collection().whereField(Constants.Firebase.Firestore.Fields.Profile.profileEmail, isEqualTo: email).getDocuments(source: source.firestore) { querySnapshot, error in 70 | if let error = error { 71 | if source == .cachedFirst { 72 | // If cached first need to do make request 73 | getProfile(email: email, source: .default, completion: completion) 74 | } else { 75 | debug("SPProfiling/Profile can't get profile for \(email), error: \(error.localizedDescription)") 76 | completion(nil, GetProfileError.convert(error)) 77 | return 78 | } 79 | } 80 | guard let documentSnapshot = querySnapshot?.documents.first else { 81 | completion(nil, GetProfileError.notFound) 82 | return 83 | } 84 | if let profileModel = Converter.convertToProfile(documentSnapshot) { 85 | completion(profileModel, nil) 86 | } else { 87 | completion(nil, GetProfileError.unknown) 88 | } 89 | } 90 | } 91 | 92 | static func setProfileName(_ name: String, completion: ((Error?)->Void)?) { 93 | guard let currentProfileModel = currentProfile else { 94 | completion?(EditProfileError.notEnoughPermissions) 95 | return 96 | } 97 | if let error = currentProfileModel.middleware(.editProfile(currentProfileModel)) { 98 | completion?(error) 99 | return 100 | } 101 | let name = ProfileModel.cleanProfileName(name) 102 | if let error = ProfileModel.middlewareProfileName(name) { 103 | completion?(error) 104 | return 105 | } 106 | let data: [String : Any] = [ 107 | Constants.Firebase.Firestore.Fields.Profile.profileName : name 108 | ] 109 | debug("SPProfiling/Profile/setProfileName called with \(data)") 110 | Firestore.profile_document(userID: configuredUserID).setData(data, merge: true, completion: { error in 111 | if let error = error { 112 | completion?(EditProfileError.convert(error)) 113 | } else { 114 | completion?(nil) 115 | } 116 | }) 117 | } 118 | 119 | static func setProfileEmail(_ email: String, completion: ((Error?)->Void)?) { 120 | guard let currentProfileModel = currentProfile else { 121 | completion?(EditProfileError.notEnoughPermissions) 122 | return 123 | } 124 | if let error = currentProfileModel.middleware(.editProfile(currentProfileModel)) { 125 | completion?(error) 126 | return 127 | } 128 | if let error = ProfileModel.middlewareProfileEmail(email) { 129 | completion?(error) 130 | return 131 | } 132 | let data: [String : Any] = [ 133 | Constants.Firebase.Firestore.Fields.Profile.profileEmail : email 134 | ] 135 | debug("SPProfiling/Profile/setProfileEmail called with \(data)") 136 | Firestore.profile_document(userID: configuredUserID).setData(data, merge: true, completion: { error in 137 | if let error = error { 138 | completion?(EditProfileError.convert(error)) 139 | } else { 140 | completion?(nil) 141 | } 142 | }) 143 | } 144 | 145 | static func setProfileCreatedDate(_ date: Date, completion: ((Error?)->Void)?) { 146 | guard let currentProfileModel = currentProfile else { 147 | completion?(EditProfileError.notEnoughPermissions) 148 | return 149 | } 150 | if let error = currentProfileModel.middleware(.editProfile(currentProfileModel)) { 151 | completion?(error) 152 | return 153 | } 154 | let data: [String : Any] = [ 155 | Constants.Firebase.Firestore.Fields.Profile.profileCreatedDate : Timestamp(date: date) 156 | ] 157 | debug("SPProfiling/Profile/setProfileCreatedDate called with \(data)") 158 | Firestore.profile_document(userID: configuredUserID).setData(data, merge: true, completion: { error in 159 | if let error = error { 160 | completion?(EditProfileError.convert(error)) 161 | } else { 162 | completion?(nil) 163 | } 164 | }) 165 | } 166 | 167 | static func saveDevice(_ deviceModel: ProfileDeviceModel, completion: ((Error?)->Void)?) { 168 | guard let currentProfileModel = currentProfile else { 169 | completion?(EditProfileError.notEnoughPermissions) 170 | return 171 | } 172 | if let error = currentProfileModel.middleware(.editProfile(currentProfileModel)) { 173 | completion?(error) 174 | return 175 | } 176 | let data = Converter.convertToData(deviceModel) 177 | debug("SPProfiling/Profile/saveDevice called with \(data)") 178 | Firestore.profile_document(userID: currentProfileModel.id).setData([ 179 | Constants.Firebase.Firestore.Fields.Profile.profileDevices : [ 180 | deviceModel.id : Converter.convertToData(deviceModel) 181 | ] 182 | ], merge: true) { error in 183 | if let error = error { 184 | completion?(EditProfileError.convert(error)) 185 | } else { 186 | completion?(nil) 187 | } 188 | } 189 | } 190 | 191 | static func deleteProfile(completion: ((Error?)->Void)?) { 192 | guard let currentProfileModel = currentProfile else { 193 | completion?(DeleteProfileError.notEnoughPermissions) 194 | return 195 | } 196 | if let error = currentProfileModel.middleware(.deleteProfile(currentProfileModel)) { 197 | completion?(error) 198 | return 199 | } 200 | 201 | 202 | reset() 203 | 204 | Firestore.profile_document(userID: currentProfileModel.id).delete { error in 205 | if let error = error { 206 | completion?(DeleteProfileError.convert(error)) 207 | } else { 208 | print("document deleted") 209 | completion?(nil) 210 | } 211 | } 212 | } 213 | } 214 | 215 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Services/Profile+Avatar.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import FirebaseStorage 24 | 25 | extension SPProfiling.Profile { 26 | 27 | public static func getAvatarURL(of profileModel: ProfileModel, completion: @escaping (URL?, Error?)->Void) { 28 | guard let currentProfileModel = currentProfile else { 29 | completion(nil, GetAvatarError.notEnoughPermissions) 30 | return 31 | } 32 | if let error = currentProfileModel.middleware(.getProfilveAvatar) { 33 | completion(nil, error) 34 | return 35 | } 36 | let userID = profileModel.id 37 | if let data = getCachedURL(userID: userID) { 38 | completion(data.url, nil) 39 | } else { 40 | let avatarStorageReference = Storage.profile_avatar(userID: profileModel.id) 41 | avatarStorageReference.downloadURL { url, error in 42 | self.cleanCachedAvatarURLs(userID: userID) 43 | self.cacheAvatar(url: url, userID: userID) 44 | completion(url, error == nil ? nil : GetAvatarError.notReachable) 45 | } 46 | } 47 | } 48 | 49 | public static func setAvatar(_ image: UIImage, completion: ((Error?)->Void)?) { 50 | guard let currentProfileModel = currentProfile else { 51 | completion?(SetAvatarError.notEnoughPermissions) 52 | return 53 | } 54 | if let error = currentProfileModel.middleware(.setProfilveAvatar(currentProfileModel)) { 55 | completion?(error) 56 | return 57 | } 58 | let cleanAvatar = ProfileModel.cleanAvatar(image: image) ?? image 59 | if let error = ProfileModel.middlewareAvatar(cleanAvatar) { 60 | completion?(error) 61 | return 62 | } 63 | guard let avatarData = cleanAvatar.jpegData(compressionQuality: 1) else { 64 | completion?(SetAvatarError.invalidData) 65 | return 66 | } 67 | let metadata = StorageMetadata() 68 | metadata.contentType = "image/jpeg" 69 | let avatarStorageReference = Storage.profile_avatar(userID: configuredUserID) 70 | avatarStorageReference.putData(avatarData, metadata: metadata) { metadata, error in 71 | self.cleanCachedAvatarURLs(userID: configuredUserID) 72 | if let _ = error { 73 | completion?(SetAvatarError.notReachable) 74 | } else { 75 | completion?(nil) 76 | } 77 | } 78 | } 79 | 80 | public static func deleteAvatar(completion: ((DeleteAvatarError?)->Void)?) { 81 | let avatarStorageReference = Storage.profile_avatar(userID: configuredUserID) 82 | avatarStorageReference.delete { error in 83 | self.cleanCachedAvatarURLs(userID: configuredUserID) 84 | if let _ = error { 85 | completion?(.notReachable) 86 | return 87 | } else { 88 | completion?(nil) 89 | } 90 | } 91 | } 92 | 93 | // MARK: - Cache 94 | 95 | private static func getCachedURL(userID: String) -> CachedAvatar? { 96 | cachedAvatarURLs.first(where: { $0.userID == userID }) 97 | } 98 | 99 | private static func cacheAvatar(url: URL?, userID: String) { 100 | cachedAvatarURLs.append((userID: userID, url: url)) 101 | } 102 | 103 | private static func cleanCachedAvatarURLs(userID: String) { 104 | cachedAvatarURLs = cachedAvatarURLs.filter({ $0.userID != userID }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Services/Profile+Convertor.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | import FirebaseFirestore 24 | 25 | extension SPProfiling.Profile { 26 | 27 | class Converter { 28 | 29 | // MARK: - Profile 30 | 31 | static func convertToProfile(_ snapshot: DocumentSnapshot) -> ProfileModel? { 32 | let profileID = snapshot.documentID 33 | let name = snapshot.get(Constants.Firebase.Firestore.Fields.Profile.profileName) as? String 34 | let email = snapshot.get(Constants.Firebase.Firestore.Fields.Profile.profileEmail) as? String 35 | let createdDate = (snapshot.get(Constants.Firebase.Firestore.Fields.Profile.profileCreatedDate) as? Timestamp)?.dateValue() 36 | let devices: [ProfileDeviceModel] = { 37 | guard let devicesData = snapshot.get(Constants.Firebase.Firestore.Fields.Profile.profileDevices) as? [String : [String : Any]] else { return [] } 38 | var devices: [ProfileDeviceModel] = [] 39 | for deviceData in devicesData { 40 | if let deviceModel = convertToDevice(deviceData) { 41 | devices.append(deviceModel) 42 | } 43 | } 44 | return devices 45 | }() 46 | let profileModel = ProfileModel.init(id: profileID, name: name, email: email, createdDate: createdDate, devices: devices) 47 | return profileModel 48 | } 49 | 50 | static func convertToData(_ model: ProfileModel) -> [String: Any] { 51 | var profileData: [String: Any] = [:] 52 | if let name = model.name { 53 | profileData[Constants.Firebase.Firestore.Fields.Profile.profileName] = name 54 | } 55 | if let email = model.email { 56 | let formattedEmail = email.lowercased() 57 | profileData[Constants.Firebase.Firestore.Fields.Profile.profileEmail] = formattedEmail 58 | } 59 | if let createdDate = model.createdDate { 60 | profileData[Constants.Firebase.Firestore.Fields.Profile.profileCreatedDate] = Timestamp(date: createdDate) 61 | } 62 | var devicesData: [String : Any] = [:] 63 | for deviceModel in model.devices { 64 | devicesData[deviceModel.id] = convertToData(deviceModel) 65 | } 66 | if !devicesData.isEmpty { 67 | profileData[Constants.Firebase.Firestore.Fields.Profile.profileDevices] = devicesData 68 | } 69 | return profileData 70 | } 71 | 72 | // MARK: - Device 73 | 74 | static func convertToDevice(_ deviceData: Dictionary.Element) -> ProfileDeviceModel? { 75 | let deviceID = deviceData.key 76 | guard let deviceName = deviceData.value[Constants.Firebase.Firestore.Fields.Profile.deviceName] as? String else { return nil } 77 | guard let deviceTypeID = deviceData.value[Constants.Firebase.Firestore.Fields.Profile.deviceType] as? String else { return nil } 78 | guard let deviceType = ProfileDeviceModel.DeviceType(rawValue: deviceTypeID) else { return nil } 79 | let fcmToken = deviceData.value[Constants.Firebase.Firestore.Fields.Profile.deviceFCMToken] as? String 80 | guard let languageCode = deviceData.value[Constants.Firebase.Firestore.Fields.Profile.deviceLanguageCode] as? String else { return nil } 81 | guard let addedDate = (deviceData.value[Constants.Firebase.Firestore.Fields.Profile.deviceAddedDate] as? Timestamp)?.dateValue() else { return nil } 82 | return ProfileDeviceModel(id: deviceID, name: deviceName, type: deviceType, fcmToken: fcmToken, languageCode: languageCode, addedDate: addedDate) 83 | } 84 | 85 | static func convertToData(_ deviceModel: ProfileDeviceModel) -> [String: Any] { 86 | var data: [String : Any] = [:] 87 | data[Constants.Firebase.Firestore.Fields.Profile.deviceName] = deviceModel.name 88 | data[Constants.Firebase.Firestore.Fields.Profile.deviceType] = deviceModel.type.id 89 | if let fcmToken = deviceModel.fcmToken { 90 | data[Constants.Firebase.Firestore.Fields.Profile.deviceFCMToken] = fcmToken 91 | } 92 | data[Constants.Firebase.Firestore.Fields.Profile.deviceLanguageCode] = deviceModel.languageCode 93 | data[Constants.Firebase.Firestore.Fields.Profile.deviceAddedDate] = Timestamp(date: deviceModel.addedDate) 94 | return data 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Services/Profile+Device.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import SparrowKit 24 | import SPFirebaseMessaging 25 | 26 | extension SPProfiling.Profile { 27 | 28 | class Device { 29 | 30 | static func configure(deviceDidChangedWork: @escaping ()->Void) { 31 | SPFirebaseMessaging.addFCMTokenDidChangeListener { fcmToken in 32 | if UserDefaults.Values.firebase_fcm_token != fcmToken { 33 | UserDefaults.Values.firebase_fcm_token = fcmToken 34 | debug("SPProfiling/Profile/Device got new FCM token: \(fcmToken ?? "nil")") 35 | deviceDidChangedWork() 36 | } 37 | } 38 | } 39 | 40 | static func reset() { 41 | SPFirebaseMessaging.removeFCMTokenDidChangeListener() 42 | } 43 | 44 | static func makeCurrentDevice() -> ProfileDeviceModel { 45 | return .init( 46 | id: deviceID, 47 | name: deviceName, 48 | type: deviceType, 49 | fcmToken: fcmToken, 50 | languageCode: SPLocale.current.identifier, 51 | addedDate: Date() 52 | ) 53 | } 54 | 55 | static var deviceID: String { 56 | UIDevice.current.identifierForVendor?.uuidString ?? "invalid_device_id" 57 | } 58 | 59 | static var deviceName: String { 60 | UIDevice.current.name 61 | } 62 | 63 | static var fcmToken: String? { 64 | UserDefaults.Values.firebase_fcm_token 65 | } 66 | 67 | static var deviceType: ProfileDeviceModel.DeviceType { 68 | switch UIDevice.current.userInterfaceIdiom { 69 | case .phone: return .phone 70 | case .pad: return .pad 71 | case .mac: return .desktop 72 | default: return .phone 73 | } 74 | } 75 | 76 | // MARK: - Singltone 77 | 78 | private static var shared = Device() 79 | private init() {} 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Services/Profile+Firebase.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | import FirebaseFirestore 24 | import FirebaseStorage 25 | 26 | extension SPProfiling.Profile { 27 | 28 | enum Firestore { 29 | 30 | public static func profiles_collection() -> CollectionReference { 31 | let firestore = FirebaseFirestore.Firestore.firestore() 32 | return firestore.collection(Constants.Firebase.Firestore.Paths.profiles) 33 | } 34 | 35 | public static func profile_document(userID: String) -> DocumentReference { 36 | profiles_collection().document(userID) 37 | } 38 | } 39 | 40 | enum Storage { 41 | 42 | public static func profiles_folder() -> StorageReference { 43 | let storage = FirebaseStorage.Storage.storage() 44 | return storage.reference().child(Constants.Firebase.Storage.Paths.profiles) 45 | } 46 | 47 | public static func profile_avatar(userID: String) -> StorageReference { 48 | profiles_folder().child(userID).child(Constants.Firebase.Storage.Paths.avatar_filename) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Services/Profile+Observer.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import Foundation 23 | import SparrowKit 24 | import FirebaseFirestore 25 | 26 | extension SPProfiling.Profile { 27 | 28 | class Observer { 29 | 30 | // MARK: - Public 31 | 32 | static var isObserving: Bool { 33 | return shared.observer != nil 34 | } 35 | 36 | static func observeProfile(userID: String, profileDidChangedWork: @escaping ()->Void) { 37 | if isObserving { stopObserving() } 38 | debug("SPProfiling/Profile/Observer configure with userID \(userID). Start observing.") 39 | shared.observer = SPProfiling.Profile.Firestore.profile_document(userID: userID).addSnapshotListener({ documentSnapshot, error in 40 | if let error = error { 41 | debug("SPProfiling/Profile/Observer can't get profile, error: \(error.localizedDescription)") 42 | return 43 | } 44 | guard let documentSnapshot = documentSnapshot else { return } 45 | if let profileModel = SPProfiling.Profile.Converter.convertToProfile(documentSnapshot) { 46 | shared.cachedProfile = profileModel 47 | profileDidChangedWork() 48 | } 49 | }) 50 | } 51 | 52 | static func stopObserving() { 53 | guard isObserving else { return } 54 | debug("SPProfiling/Profile/Observer stop Profile observing.") 55 | shared.observer?.remove() 56 | shared.observer = nil 57 | shared.cachedProfile = nil 58 | } 59 | 60 | static func getCachedProfile() -> ProfileModel? { 61 | return shared.cachedProfile 62 | } 63 | 64 | // MARK: - Singltone 65 | 66 | private var observer: ListenerRegistration? 67 | private var cachedProfile: ProfileModel? 68 | private static var shared = Observer() 69 | private init() {} 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/SPProfiling/Services/Profile.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // Copyright © 2022 Ivan Vorobei (hello@ivanvorobei.io) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | import UIKit 23 | import SparrowKit 24 | 25 | extension SPProfiling { 26 | 27 | class Profile { 28 | 29 | // MARK: - Public 30 | 31 | static func configure(userID: String) { 32 | debug("SPProfiling/Profile configure with userID \(userID).") 33 | shared.userID = userID 34 | Observer.observeProfile(userID: userID, profileDidChangedWork: { 35 | NotificationCenter.default.post(name: SPProfiling.didReloadedProfile) 36 | self.checkCurrentDevice() 37 | }) 38 | Device.configure(deviceDidChangedWork: { 39 | self.checkCurrentDevice() 40 | }) 41 | } 42 | 43 | static func reset() { 44 | if shared.userID == nil { return } 45 | debug("SPProfiling/Profile reset.") 46 | Observer.stopObserving() 47 | shared.userID = nil 48 | Device.reset() 49 | } 50 | 51 | // MARK: - Data 52 | 53 | static var currentProfile: ProfileModel? { 54 | guard let userID = shared.userID else { 55 | debug("SPProfiling/Profile Profile don't configured, return nil for current profile.") 56 | return nil 57 | } 58 | if let observedProfile = Observer.getCachedProfile() { 59 | if observedProfile.id == userID { 60 | return observedProfile 61 | } 62 | } 63 | return ProfileModel(id: userID, devices: []) 64 | } 65 | 66 | // MARK: - Actions 67 | 68 | private static func checkCurrentDevice() { 69 | let validDevice = Device.makeCurrentDevice() 70 | guard let savedDevice = currentProfile?.devices.first(where: { $0.id == Device.deviceID }) else { 71 | debug("SPProfiling/Profile current device not in list, adding it.") 72 | saveDevice(validDevice, completion: nil) 73 | return 74 | } 75 | let shoudUpdate: Bool = { 76 | if savedDevice.name != validDevice.name { return true } 77 | if savedDevice.languageCode != validDevice.languageCode { return true } 78 | if savedDevice.fcmToken != validDevice.fcmToken { return true } 79 | if savedDevice.type != validDevice.type { return true } 80 | return false 81 | }() 82 | if shoudUpdate { 83 | debug("SPProfiling/Profile current device changed, updating to new.") 84 | saveDevice(validDevice, completion: nil) 85 | } 86 | } 87 | 88 | // MARK: - Singltone 89 | 90 | private var userID: String? = nil 91 | private var cachedAvatarURLs: [(userID: String, url: URL?)] = [] 92 | private static var shared = Profile() 93 | private init() {} 94 | 95 | // MARK: - Access 96 | 97 | internal static var configuredUserID: String { Profile.shared.userID! } 98 | 99 | internal static var cachedAvatarURLs: [CachedAvatar] { 100 | get { shared.cachedAvatarURLs } 101 | set { shared.cachedAvatarURLs = newValue } 102 | } 103 | 104 | typealias CachedAvatar = (userID: String, url: URL?) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | Here provided ideas or features which will be implemented soon. 4 | --------------------------------------------------------------------------------