├── .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 |
--------------------------------------------------------------------------------