├── .gitignore
├── mentorship ios
├── it.lproj
│ └── LaunchScreen.strings
├── pl.lproj
│ ├── LaunchScreen.strings
│ └── Localizable.strings
├── Assets.xcassets
│ ├── Contents.json
│ ├── first.imageset
│ │ ├── first.pdf
│ │ └── Contents.json
│ ├── second.imageset
│ │ ├── second.pdf
│ │ └── Contents.json
│ ├── mentorship_system_logo.imageset
│ │ ├── mentorship_system_logo.png
│ │ ├── mentorship_system_logo_dark.png
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── Config
│ ├── Config.plist
│ └── Config.swift
├── Constants
│ ├── UserDefaultsConstants.swift
│ ├── ImageNameConstants.swift
│ ├── URLStringConstants.swift
│ ├── DesignConstants.swift
│ └── LocalizableStringConstants.swift
├── mentorship_ios.xcdatamodeld
│ ├── .xccurrentversion
│ └── mentorship_ios.xcdatamodel
│ │ └── contents
├── mentorship ios.entitlements
├── Modifiers
│ ├── ErrorText.swift
│ ├── AllPadding.swift
│ └── KeyboardAware.swift
├── Extensions
│ └── UserDefaults.swift
├── Protocols
│ ├── TaskProperties.swift
│ └── ProfileProperties.swift
├── Models
│ ├── AuthModel.swift
│ ├── TaskModel.swift
│ ├── ChangePasswordModel.swift
│ ├── LoginModel.swift
│ ├── SignUpModel.swift
│ ├── SettingsModel.swift
│ ├── TaskCommentsModel.swift
│ ├── MembersModel.swift
│ ├── RequestModel.swift
│ ├── RelationModel.swift
│ ├── ProfileModel.swift
│ └── HomeModel.swift
├── ViewModels
│ ├── DetailListCellViewModel.swift
│ ├── SettingsViewModel.swift
│ ├── SignUpViewModel.swift
│ ├── LoginViewModel.swift
│ ├── ChangePasswordViewModel.swift
│ ├── MembersViewModel.swift
│ ├── HomeViewModel.swift
│ ├── TaskCommentsViewModel.swift
│ ├── RelationViewModel.swift
│ └── ProfileViewModel.swift
├── CustomStyles
│ ├── RoundFilledTextFieldStyle.swift
│ └── BigBoldButtonStyle.swift
├── Views
│ ├── UtilityViews
│ │ ├── ActivityIndicator.swift
│ │ ├── WebView.swift
│ │ ├── SocialSignInButtons.swift
│ │ ├── ActivityWithText.swift
│ │ ├── MemberDetailCell.swift
│ │ ├── CommonProfileSection.swift
│ │ └── SearchNavigation.swift
│ ├── Profile
│ │ ├── SupportingView
│ │ │ └── ProfileEditField.swift
│ │ └── ProfileSummary.swift
│ ├── Home
│ │ ├── SupportingViews
│ │ │ ├── TasksDoneSection.swift
│ │ │ ├── RelationListCell.swift
│ │ │ └── TasksToDoSection.swift
│ │ ├── RelationDetailList.swift
│ │ └── Home.swift
│ ├── Members
│ │ ├── SupportingViews
│ │ │ └── MembersListCell.swift
│ │ ├── MemberDetail.swift
│ │ ├── Members.swift
│ │ └── SendRequest.swift
│ ├── Settings
│ │ └── IndividualSetting
│ │ │ ├── About.swift
│ │ │ └── ChangePassword.swift
│ ├── Tasks
│ │ ├── TasksSection.swift
│ │ └── TaskCommentCell.swift
│ ├── Relation
│ │ └── AddTask.swift
│ └── Registration
│ │ └── Login.swift
├── ContentView.swift
├── en.lproj
│ └── Localizable.strings
├── Utilities
│ └── UIHelper.swift
├── Managers
│ ├── NetworkManager.swift
│ ├── KeychainManager.swift
│ └── SocialSignIn.swift
├── Utility
│ └── DesignConstants.swift
├── Base.lproj
│ └── LaunchScreen.storyboard
├── Service
│ ├── Networking
│ │ ├── RequestActionAPI.swift
│ │ ├── SignUpAPI.swift
│ │ ├── HomeAPI.swift
│ │ ├── ProfileAPI.swift
│ │ ├── SettingsAPI.swift
│ │ ├── LoginAPI.swift
│ │ ├── RelationAPI.swift
│ │ ├── TaskCommentsAPI.swift
│ │ └── MembersAPI.swift
│ └── ServiceProtocols.swift
├── TabBar.swift
├── Info.plist
└── SceneDelegate.swift
├── mentorship ios.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── WorkspaceSettings.xcsettings
│ │ └── IDEWorkspaceChecks.plist
├── xcshareddata
│ ├── IDETemplateMacros.plist
│ └── xcschemes
│ │ ├── mentorship iosTests.xcscheme
│ │ ├── mentorship iosUITests.xcscheme
│ │ └── mentorship ios.xcscheme
└── xcuserdata
│ └── yugantarjain.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
├── mentorship ios.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Podfile
├── .travis.yml
├── .github
├── ISSUE_TEMPLATE
│ ├── user-story-task.md
│ ├── user-story.md
│ ├── feature_request.md
│ └── bug_report.md
├── CODEOWNERS
├── contributing_guidelines.md
├── PULL_REQUEST_TEMPLATE.md
├── config.yml
└── reporting_guidelines.md
├── mentorship iosTests
├── Info.plist
├── MockURLProtocol.swift
├── MentorshipTests.swift
└── HomeTests.swift
├── mentorship iosUITests
├── Info.plist
└── MentorshipUITests.swift
├── Podfile.lock
├── Docs
├── Configuring Remotes.md
├── Contributing and Developing.md
└── Screenshots.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | xcuserdata/
2 | .DS_Store
--------------------------------------------------------------------------------
/mentorship ios/it.lproj/LaunchScreen.strings:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/mentorship ios/pl.lproj/LaunchScreen.strings:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/mentorship ios/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/mentorship ios/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/mentorship ios/Assets.xcassets/first.imageset/first.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anitab-org/mentorship-ios/HEAD/mentorship ios/Assets.xcassets/first.imageset/first.pdf
--------------------------------------------------------------------------------
/mentorship ios/Assets.xcassets/second.imageset/second.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anitab-org/mentorship-ios/HEAD/mentorship ios/Assets.xcassets/second.imageset/second.pdf
--------------------------------------------------------------------------------
/mentorship ios/pl.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings
3 | Created on 04/06/20.
4 | Created for AnitaB.org Mentorship-iOS
5 | */
6 |
7 | "terms and conditions" = "DEMO POLISH TNC STRING";
8 |
--------------------------------------------------------------------------------
/mentorship ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/mentorship ios/Assets.xcassets/mentorship_system_logo.imageset/mentorship_system_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anitab-org/mentorship-ios/HEAD/mentorship ios/Assets.xcassets/mentorship_system_logo.imageset/mentorship_system_logo.png
--------------------------------------------------------------------------------
/mentorship ios/Assets.xcassets/mentorship_system_logo.imageset/mentorship_system_logo_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anitab-org/mentorship-ios/HEAD/mentorship ios/Assets.xcassets/mentorship_system_logo.imageset/mentorship_system_logo_dark.png
--------------------------------------------------------------------------------
/mentorship ios/Assets.xcassets/first.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "first.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/mentorship ios/Assets.xcassets/second.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "second.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/mentorship ios/Config/Config.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | googleAuthClientId
6 | <google-auth-client-id>
7 |
8 |
9 |
--------------------------------------------------------------------------------
/mentorship ios.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/mentorship ios.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/mentorship ios.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/mentorship ios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/mentorship ios/Constants/UserDefaultsConstants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaultsConstants.swift
3 | // Created on 12/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 |
9 | struct UserDefaultsConstants {
10 | static let isLoggedIn = "isLoggedIn"
11 | static let profile = "userProfile"
12 | }
13 |
--------------------------------------------------------------------------------
/mentorship ios/mentorship_ios.xcdatamodeld/.xccurrentversion:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | _XCCurrentVersionName
6 | mentorship_ios.xcdatamodel
7 |
8 |
9 |
--------------------------------------------------------------------------------
/mentorship ios/mentorship ios.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.applesignin
6 |
7 | Default
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/mentorship ios/mentorship_ios.xcdatamodeld/mentorship_ios.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/mentorship ios.xcodeproj/xcshareddata/IDETemplateMacros.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | FILEHEADER
6 |
7 | // ___FILENAME___
8 | // Created on ___DATE___
9 | // Created for AnitaB.org Mentorship-iOS
10 | //
11 |
12 |
13 |
--------------------------------------------------------------------------------
/mentorship ios/Modifiers/ErrorText.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ErrorText.swift
3 | // Created on 13/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct ErrorText: ViewModifier {
10 | func body(content: Content) -> some View {
11 | content
12 | .font(DesignConstants.Fonts.userError)
13 | .foregroundColor(DesignConstants.Colors.userError)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/mentorship ios.xcodeproj/xcuserdata/yugantarjain.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | mentorship ios.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/mentorship ios/Extensions/UserDefaults.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaults.swift
3 | // Created on 08/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 |
9 | extension UserDefaults {
10 | //used to do KVO using Combine
11 | //observed for login state, login or home screen showed accordingly.
12 | //Used in ContentView.swift
13 | @objc dynamic var isLoggedIn: Bool {
14 | return bool(forKey: UserDefaultsConstants.isLoggedIn)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/mentorship ios/Protocols/TaskProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TaskProperties.swift
3 | // Created on 30/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | //protocol used for TaskStructure propoerties
8 | //helps in reusing TasksList in home screen and relation screen
9 |
10 | protocol TaskStructureProperties: Identifiable {
11 | var id: Int? { get }
12 | var description: String? { get }
13 | var createdAt: Double? { get }
14 | var completedAt: Double? { get }
15 | }
16 |
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment the next line to define a global platform for your project
2 | # platform :ios, '9.0'
3 |
4 | target 'mentorship ios' do
5 | # Comment the next line if you don't want to use dynamic frameworks
6 | use_frameworks!
7 |
8 | # Pods for mentorship ios
9 | pod 'GoogleSignIn'
10 |
11 | target 'mentorship iosTests' do
12 | inherit! :search_paths
13 | # Pods for testing
14 | end
15 |
16 | target 'mentorship iosUITests' do
17 | # Pods for testing
18 | end
19 |
20 | end
21 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | before_install:
2 | - gem install cocoapods
3 | install:
4 | - brew update && brew upgrade swiftlint
5 | - pod install
6 | language: swift
7 | osx_image: xcode11.6
8 | xcode_workspace: mentorship ios.xcworkspace # path to your xcodeproj folder
9 | xcode_scheme: mentorship iosTests
10 | xcode_destination: platform=iOS Simulator,OS=13.6,name=iPhone 8
11 | script:
12 | - swiftlint
13 | - xcodebuild test -workspace 'mentorship ios.xcworkspace' -scheme 'mentorship iosTests' -destination 'platform=iOS Simulator,OS=13.6,name=iPhone 8'
14 |
--------------------------------------------------------------------------------
/mentorship ios/Models/AuthModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AuthModel.swift
3 | // Created on 08/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 | import Combine
9 |
10 | final class AuthModel: ObservableObject {
11 | @Published var isLogged: Bool?
12 | private var cancellable: AnyCancellable?
13 |
14 | init() {
15 | //observe login state
16 | cancellable = UserDefaults.standard
17 | .publisher(for: \.isLoggedIn)
18 | .sink {
19 | self.isLogged = $0
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/mentorship ios/ViewModels/DetailListCellViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailListCellViewModel.swift
3 | // Created on 22/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 | import Combine
9 |
10 | class DetailListCellViewModel: ObservableObject {
11 | var requestData: RequestStructure
12 | var endDate: String
13 |
14 | init(data: RequestStructure) {
15 | requestData = data
16 | endDate = DesignConstants.DateFormat.mediumDate.string(from: Date(timeIntervalSince1970: requestData.endDate ?? 0))
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/mentorship ios/Models/TaskModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TaskModel.swift
3 | // Created on 05/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | struct TaskStructure: Codable, Identifiable, Equatable {
8 | let id: Int?
9 | let description: String?
10 | let isDone: Bool?
11 | let createdAt: Double?
12 | let completedAt: Double?
13 |
14 | enum CodingKeys: String, CodingKey {
15 | case id, description
16 | case isDone = "is_done"
17 | case createdAt = "created_at"
18 | case completedAt = "completed_at"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/user-story-task.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: User Story task
3 | about: Create your development plans here
4 |
5 | ---
6 |
7 | ## Description
8 | As a [USER],
9 | I need [TO DO THIS],
10 | so that I can [ACCOMPLISH THAT].
11 |
12 | ## Mocks
13 | [INSERT RELEVANT PNG FILE]
14 |
15 | ## Acceptance Criteria
16 | ### Update [Required]
17 | - [ ] [LIST ITEMS]
18 | ### Enhancement to Update [Optional]
19 | - [ ] [LIST ITEMS]
20 |
21 | ## Definition of Done
22 | - [ ] All of the required items are completed.
23 | - [ ] Approval by 1 mentor.
24 |
25 | ## Estimation
26 | [INSERT NUMBER HERE] hours
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/user-story.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: User story
3 | about: Create your development plans and work tasks here.
4 |
5 | ---
6 |
7 | ## Description
8 | As a [USER],
9 | I need [TO DO THIS],
10 | so that I can [ACCOMPLISH THAT].
11 |
12 | ## Mocks
13 | [INSERT RELEVANT PNG FILE]
14 |
15 | ## Acceptance Criteria
16 | ### Update [Required]
17 | - [ ] [LIST ITEMS]
18 | ### Enhancement to Update [Optional]
19 | - [ ] [LIST ITEMS]
20 |
21 | ## Definition of Done
22 | - [ ] All of the required items are completed.
23 | - [ ] Approval by 1 mentor.
24 |
25 | ## Estimation
26 | [INSERT NUMBER HERE] hours
27 |
--------------------------------------------------------------------------------
/mentorship ios/Modifiers/AllPadding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AllPadding.swift
3 | // Created on 06/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct AllPadding: ViewModifier {
10 | func body(content: Content) -> some View {
11 | content
12 | .padding(.top, DesignConstants.Screen.Padding.topPadding)
13 | .padding(.bottom, DesignConstants.Screen.Padding.bottomPadding)
14 | .padding(.leading, DesignConstants.Screen.Padding.leadingPadding)
15 | .padding(.trailing, DesignConstants.Screen.Padding.trailingPadding)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # This is a comment.
2 | # Each line is a file pattern followed by one or more owners.
3 |
4 | # These owners will be the default owners for everything in
5 | # the repo. Unless a later match takes precedence,
6 | # @global-owner1 and @global-owner2 will be requested for
7 | # review when someone opens a pull request.
8 | # Please add names of code owners here and tag the below
9 | # Ginny Smith
10 | * @sunjunkie
11 |
12 | # Later we can add specific files codeowners to give us more
13 | # control over this for example assests manager will be tagged
14 | # only when we have a change made in assets folder
15 | # /assets/ @assets-manager
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/mentorship ios/Models/ChangePasswordModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsModel.swift
3 | // Created on 26/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | class ChangePasswordModel {
8 | struct ChangePasswordUploadData: Encodable {
9 | var currentPassword: String
10 | var newPassword: String
11 |
12 | enum CodingKeys: String, CodingKey {
13 | case currentPassword = "current_password"
14 | case newPassword = "new_password"
15 | }
16 | }
17 |
18 | struct ChangePasswordResponseData: Encodable {
19 | let message: String?
20 | var success: Bool
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/mentorship ios/CustomStyles/RoundFilledTextFieldStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomTextField.swift
3 | // Created on 03/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct RoundFilledTextFieldStyle: TextFieldStyle {
10 | // swiftlint:disable:next all
11 | func _body(configuration: TextField<_Label>) -> some View {
12 | configuration
13 | .padding(DesignConstants.Padding.textFieldFrameExpansion)
14 | .background(
15 | RoundedRectangle(cornerRadius: DesignConstants.CornerRadius.preferredCornerRadius)
16 | .fill(DesignConstants.Colors.secondaryBackground)
17 | )
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/mentorship ios/Views/UtilityViews/ActivityIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ActivityIndicator.swift
3 | // Created on 06/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct ActivityIndicator: UIViewRepresentable {
10 | @Binding var isAnimating: Bool
11 | var style: UIActivityIndicatorView.Style = .medium
12 |
13 | func makeUIView(context: UIViewRepresentableContext) -> UIActivityIndicatorView {
14 | return UIActivityIndicatorView(style: style)
15 | }
16 |
17 | func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext) {
18 | isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/mentorship ios/Config/Config.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Config.swift
3 | // Created on 19/08/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 |
9 | struct ConfigValues {
10 | static func get() -> Config {
11 | guard let url = Bundle.main.url(forResource: "Config", withExtension: "plist") else {
12 | fatalError("Config.plist not found")
13 | }
14 | do {
15 | let data = try Data(contentsOf: url)
16 | let decoder = PropertyListDecoder()
17 | return try decoder.decode(Config.self, from: data)
18 | } catch let error {
19 | fatalError(error.localizedDescription)
20 | }
21 | }
22 | }
23 |
24 | struct Config: Decodable {
25 | let googleAuthClientId: String
26 | }
27 |
--------------------------------------------------------------------------------
/mentorship ios/Models/LoginModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoginModel.swift
3 | // Created on 05/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | final class LoginModel {
8 |
9 | // MARK: - Structures
10 | struct LoginUploadData: Encodable {
11 | var username: String
12 | var password: String
13 | }
14 |
15 | struct SocialSignInData: Encodable {
16 | var idToken: String
17 | var name: String
18 | var email: String
19 |
20 | enum CodingKeys: String, CodingKey {
21 | case idToken = "id_token"
22 | case name, email
23 | }
24 | }
25 |
26 | enum SocialSignInType {
27 | case google, apple
28 | }
29 |
30 | struct LoginResponseData: Encodable {
31 | var message: String?
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/mentorship iosTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/mentorship ios/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Created on 30/05/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 | import Combine
9 |
10 | struct ContentView: View {
11 | @State private var selection = 0
12 | @ObservedObject var authModel = AuthModel()
13 |
14 | @ViewBuilder var body: some View {
15 | if authModel.isLogged! {
16 | TabBar(selection: $selection)
17 | .onAppear {
18 | // reset selection and show home page whenever login done
19 | self.selection = 0
20 | }
21 | } else {
22 | Login()
23 | }
24 | }
25 | }
26 |
27 | struct ContentView_Previews: PreviewProvider {
28 | static var previews: some View {
29 | ContentView()
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/mentorship iosUITests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/mentorship ios/Views/Profile/SupportingView/ProfileEditField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileEditField.swift
3 | // Created on 18/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct ProfileEditField: View {
10 | var type: LocalizableStringConstants.ProfileKeys
11 | @Binding var value: String
12 |
13 | var body: some View {
14 | HStack {
15 | Text(type.rawValue)
16 | .bold()
17 | .frame(width: DesignConstants.Width.listCellTitle)
18 | .multilineTextAlignment(.center)
19 | Divider()
20 | TextField(type.rawValue, text: $value)
21 | }
22 | }
23 | }
24 |
25 | struct ProfileEditField_Previews: PreviewProvider {
26 | static var previews: some View {
27 | ProfileEditField(type: .bio, value: .constant("value"))
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/mentorship ios/Views/UtilityViews/WebView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WebView.swift
3 | // Created on 25/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 | import WebKit
9 |
10 | struct WebView : UIViewRepresentable {
11 |
12 | let urlString: String
13 |
14 | func makeUIView(context: Context) -> WKWebView {
15 | return WKWebView()
16 | }
17 |
18 | func updateUIView(_ uiView: WKWebView, context: Context) {
19 | //convert string to url
20 | let url = URL(string: urlString)!
21 | //setup url request
22 | let request = URLRequest(url: url)
23 | //Load
24 | uiView.load(request)
25 | }
26 |
27 | }
28 |
29 |
30 | struct WebView_Previews: PreviewProvider {
31 | static var previews: some View {
32 | WebView(urlString: "https://www.apple.com")
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/mentorship ios/CustomStyles/BigBoldButtonStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BigBoldButton.swift
3 | // Created on 03/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct BigBoldButtonStyle: ButtonStyle {
10 | var disabled: Bool = false
11 |
12 | func makeBody(configuration: Configuration) -> some View {
13 | configuration.label
14 | .frame(width: 200)
15 | .padding(.vertical, DesignConstants.Padding.textFieldFrameExpansion)
16 | .background(DesignConstants.Colors.defaultIndigoColor)
17 | .foregroundColor(Color(.systemBackground))
18 | .cornerRadius(DesignConstants.CornerRadius.preferredCornerRadius)
19 | .opacity(configuration.isPressed ? DesignConstants.Opacity.tapHighlightingOpacity : 1.0)
20 | .opacity(disabled ? DesignConstants.Opacity.disabledViewOpacity : 1.0)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/mentorship ios/Models/SignUpModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SignUpModel.swift
3 | // Created on 05/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | final class SignUpModel {
8 |
9 | // MARK: - Structures
10 | struct SignUpUploadData: Encodable {
11 | var name: String
12 | var username: String
13 | var password: String
14 | var email: String
15 |
16 | var tncChecked: Bool
17 | var needMentoring: Bool
18 | var availableToMentor: Bool
19 |
20 | enum CodingKeys: String, CodingKey {
21 | case name, username, password, email
22 | case tncChecked = "terms_and_conditions_checked"
23 | case needMentoring = "need_mentoring"
24 | case availableToMentor = "available_to_mentor"
25 | }
26 | }
27 |
28 | struct SignUpResponseData: Encodable {
29 | var message: String?
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/mentorship ios/Views/UtilityViews/SocialSignInButtons.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SocialSignInButtons.swift
3 | // Created on 14/08/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 | import GoogleSignIn
9 | import AuthenticationServices
10 |
11 | // Google
12 | struct GoogleSignInButton: UIViewRepresentable {
13 | func makeUIView(context: Context) -> GIDSignInButton {
14 | return GIDSignInButton()
15 | }
16 |
17 | func updateUIView(_ uiView: GIDSignInButton, context: Context) {
18 | }
19 | }
20 |
21 | // Apple
22 | struct AppleSignInButton: UIViewRepresentable {
23 | let dark: Bool
24 |
25 | func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
26 | return ASAuthorizationAppleIDButton(type: .default, style: dark ? .black : .white)
27 | }
28 |
29 | func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) {
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/mentorship ios/Models/SettingsModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsModel.swift
3 | // Created on 27/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | class SettingsModel {
8 |
9 | //MARK: - Structures
10 | struct SettingsData {
11 | let settingsOptions = [
12 | ["About", "Feedback", "Change Password"],
13 | ["Logout", "Delete Account"]
14 | ]
15 |
16 | let settingsIcons = [
17 | [ImageNameConstants.SFSymbolConstants.about,
18 | ImageNameConstants.SFSymbolConstants.feedback,
19 | ImageNameConstants.SFSymbolConstants.changePassword],
20 | [ImageNameConstants.SFSymbolConstants.logout,
21 | ImageNameConstants.SFSymbolConstants.deleteAccount]
22 | ]
23 | }
24 |
25 | struct DeleteAccountResponseData: Encodable {
26 | let message: String?
27 | let success: Bool
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/mentorship ios/Models/TaskCommentsModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TaskCommentsModel.swift
3 | // Created on 28/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | class TaskCommentsModel {
8 | struct TaskCommentsResponse: Identifiable, Encodable, Comparable {
9 |
10 | // sorting logic added, to have comments in ascending order of creation date
11 | static func < (lhs: TaskCommentsModel.TaskCommentsResponse, rhs: TaskCommentsModel.TaskCommentsResponse) -> Bool {
12 | lhs.creationDate ?? 0 < rhs.creationDate ?? 0
13 | }
14 |
15 | let id: Int
16 | let userID: Int?
17 | let creationDate: Double?
18 | let comment: String?
19 | }
20 |
21 | struct PostCommentUploadData: Encodable {
22 | var comment: String
23 | }
24 |
25 | struct MessageResponse: Encodable {
26 | let message: String?
27 | let success: Bool
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/mentorship ios/Views/UtilityViews/ActivityWithText.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ActivityWithText.swift
3 | // Created on 19/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct ActivityWithText: View {
10 | @Binding var isAnimating: Bool
11 | var textType: LocalizableStringConstants.ActivityTextKeys
12 |
13 | var body: some View {
14 | HStack {
15 | ActivityIndicator(isAnimating: $isAnimating, style: .medium)
16 |
17 | Text(textType.rawValue)
18 | .font(.callout)
19 | }
20 | .padding()
21 | .padding(.horizontal)
22 | .background(DesignConstants.Colors.formBackgroundColor)
23 | .cornerRadius(DesignConstants.CornerRadius.preferredCornerRadius)
24 | }
25 | }
26 |
27 | //struct ActivityWithText_Previews: PreviewProvider {
28 | // static var previews: some View {
29 | // ActivityWithText()
30 | // }
31 | //}
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | **Describe the bug**
8 | A clear and concise description of what the bug is.
9 |
10 | **To Reproduce**
11 | Steps to reproduce the behavior:
12 | 1. Go to '...'
13 | 2. Click on '....'
14 | 3. Scroll down to '....'
15 | 4. See error
16 |
17 | **Expected behavior**
18 | A clear and concise description of what you expected to happen.
19 |
20 | **Screenshots**
21 | If applicable, add screenshots to help explain your problem.
22 |
23 | **Desktop (please complete the following information):**
24 | - OS: [e.g. iOS]
25 | - Browser [e.g. chrome, safari]
26 | - Version [e.g. 22]
27 |
28 | **Smartphone (please complete the following information):**
29 | - Device: [e.g. iPhone6]
30 | - OS: [e.g. iOS8.1]
31 | - Browser [e.g. stock browser, safari]
32 | - Version [e.g. 22]
33 |
34 | **Additional context**
35 | Add any other context about the problem here.
36 |
--------------------------------------------------------------------------------
/mentorship ios/Views/Home/SupportingViews/TasksDoneSection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TasksDoneList.swift
3 | // Created on 30/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct TasksDoneSection: View {
10 | var tasksDone: [T]?
11 |
12 | var body: some View {
13 | //Tasks Done list
14 | Section(header: Text(LocalizableStringConstants.tasksDone).font(.headline)) {
15 | ForEach(tasksDone ?? []) { task in
16 | HStack {
17 | Image(systemName: ImageNameConstants.SFSymbolConstants.taskDone)
18 | .foregroundColor(DesignConstants.Colors.defaultIndigoColor)
19 | .padding(.trailing, DesignConstants.Padding.insetListCellFrameExpansion)
20 |
21 | Text(task.description ?? "-")
22 | .font(.subheadline)
23 | }
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/mentorship ios/ViewModels/SettingsViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsViewModel.swift
3 | // Created on 27/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 | import Combine
9 |
10 | class SettingsViewModel: ObservableObject {
11 |
12 | //MARK: - Variables
13 | let settingsData = SettingsModel.SettingsData()
14 | let destinationViews = UIHelper().settingsViews
15 | @Published var deleteAccountResponseData = SettingsModel.DeleteAccountResponseData(message: "", success: false)
16 | @Published var showUserDeleteAlert = false
17 | var alertTitle = LocalizedStringKey("")
18 |
19 | // MARK: - Functions
20 | func logout() {
21 | //delete keychain item
22 | do {
23 | try KeychainManager.deleteToken()
24 | } catch {
25 | fatalError()
26 | }
27 | //go to login screen
28 | UserDefaults.standard.set(false, forKey: UserDefaultsConstants.isLoggedIn)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/mentorship ios/Modifiers/KeyboardAware.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardAware.swift
3 | // Created on 18/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 | import Combine
9 |
10 | struct KeyboardAware: ViewModifier {
11 | @State private var keyboardHeight: CGFloat = 0
12 |
13 | private var keyboardHeightPublisher: AnyPublisher {
14 | Publishers.Merge(
15 | NotificationCenter.default
16 | .publisher(for: UIResponder.keyboardWillShowNotification)
17 | .compactMap { $0.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect }
18 | .map { $0.height },
19 | NotificationCenter.default
20 | .publisher(for: UIResponder.keyboardWillHideNotification)
21 | .map { _ in CGFloat(0) }
22 | ).eraseToAnyPublisher()
23 | }
24 |
25 | func body(content: Content) -> some View {
26 | content
27 | .padding(.bottom, keyboardHeight)
28 | .onReceive(keyboardHeightPublisher) { self.keyboardHeight = $0 }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/mentorship ios/Views/UtilityViews/MemberDetailCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MemberDetailCell.swift
3 | // Created on 09/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct MemberDetailCell: View {
10 | let type: LocalizableStringConstants.ProfileKeys
11 | let value: String?
12 | let hideEmptyFields: Bool
13 |
14 | var body: some View {
15 | guard !(value?.isEmpty ?? true) || !hideEmptyFields else {
16 | return AnyView(EmptyView())
17 | }
18 | return AnyView(
19 | HStack {
20 | Text(type.rawValue).font(.subheadline)
21 | .frame(width: DesignConstants.Width.listCellTitle)
22 | .multilineTextAlignment(.center)
23 | Divider()
24 | Text(value?.isEmpty ?? false ? "-" : value ?? "-").font(.headline)
25 | }
26 | )
27 | }
28 | }
29 |
30 | struct MemberDetailCell_Previews: PreviewProvider {
31 | static var previews: some View {
32 | MemberDetailCell(type: .bio, value: "value", hideEmptyFields: true)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/mentorship ios/Views/Home/SupportingViews/RelationListCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RelationCell.swift
3 | // Created on 12/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct RelationListCell: View {
10 | var systemImageName: String
11 | var imageColor: Color
12 | var title: String
13 | var count: Int
14 |
15 | var body: some View {
16 | HStack {
17 | Image(systemName: systemImageName)
18 | .foregroundColor(imageColor)
19 | .padding(.trailing, DesignConstants.Padding.insetListCellFrameExpansion)
20 | .font(.system(size: DesignConstants.Fonts.Size.insetListIcon))
21 |
22 | Text(title)
23 | .font(.subheadline)
24 |
25 | Spacer()
26 |
27 | Text(String(count))
28 | .font(.subheadline)
29 | }
30 | }
31 | }
32 |
33 | struct RelationCell_Previews: PreviewProvider {
34 | static var previews: some View {
35 | RelationListCell(systemImageName: "xmark.circle.fill", imageColor: .blue, title: "Accepted", count: 10)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - AppAuth (1.4.0):
3 | - AppAuth/Core (= 1.4.0)
4 | - AppAuth/ExternalUserAgent (= 1.4.0)
5 | - AppAuth/Core (1.4.0)
6 | - AppAuth/ExternalUserAgent (1.4.0)
7 | - GoogleSignIn (5.0.2):
8 | - AppAuth (~> 1.2)
9 | - GTMAppAuth (~> 1.0)
10 | - GTMSessionFetcher/Core (~> 1.1)
11 | - GTMAppAuth (1.0.0):
12 | - AppAuth/Core (~> 1.0)
13 | - GTMSessionFetcher (~> 1.1)
14 | - GTMSessionFetcher (1.4.0):
15 | - GTMSessionFetcher/Full (= 1.4.0)
16 | - GTMSessionFetcher/Core (1.4.0)
17 | - GTMSessionFetcher/Full (1.4.0):
18 | - GTMSessionFetcher/Core (= 1.4.0)
19 |
20 | DEPENDENCIES:
21 | - GoogleSignIn
22 |
23 | SPEC REPOS:
24 | trunk:
25 | - AppAuth
26 | - GoogleSignIn
27 | - GTMAppAuth
28 | - GTMSessionFetcher
29 |
30 | SPEC CHECKSUMS:
31 | AppAuth: 31bcec809a638d7bd2f86ea8a52bd45f6e81e7c7
32 | GoogleSignIn: 7137d297ddc022a7e0aa4619c86d72c909fa7213
33 | GTMAppAuth: 4deac854479704f348309e7b66189e604cf5e01e
34 | GTMSessionFetcher: 6f5c8abbab8a9bce4bb3f057e317728ec6182b10
35 |
36 | PODFILE CHECKSUM: 78a8cb73e37727fdec10b33d5e8d32bbbad8444f
37 |
38 | COCOAPODS: 1.9.3
39 |
--------------------------------------------------------------------------------
/mentorship ios/ViewModels/SignUpViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SignUpViewModel.swift
3 | // Created on 21/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 | import Combine
9 |
10 | class SignUpViewModel: ObservableObject {
11 |
12 | // MARK: - Variables
13 | @Published var signUpData = SignUpModel.SignUpUploadData(name: "", username: "", password: "", email: "", tncChecked: false, needMentoring: true, availableToMentor: false)
14 | @Published var signUpResponseData = SignUpModel.SignUpResponseData(message: "")
15 | @Published var confirmPassword: String = ""
16 | @Published var availabilityPickerSelection: Int = 2
17 | @Published var inActivity: Bool = false
18 |
19 | var signupDisabled: Bool {
20 | if signUpData.name.isEmpty || signUpData.username.isEmpty || signUpData.email.isEmpty || signUpData.password.isEmpty || confirmPassword.isEmpty || !signUpData.tncChecked {
21 | return true
22 | } else {
23 | return false
24 | }
25 | }
26 |
27 | // MARK: - Functions
28 | func update(using data: SignUpModel.SignUpResponseData) {
29 | signUpResponseData = data
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/mentorship ios/Assets.xcassets/mentorship_system_logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "mentorship_system_logo.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "appearances" : [
10 | {
11 | "appearance" : "luminosity",
12 | "value" : "dark"
13 | }
14 | ],
15 | "filename" : "mentorship_system_logo_dark.png",
16 | "idiom" : "universal",
17 | "scale" : "1x"
18 | },
19 | {
20 | "idiom" : "universal",
21 | "scale" : "2x"
22 | },
23 | {
24 | "appearances" : [
25 | {
26 | "appearance" : "luminosity",
27 | "value" : "dark"
28 | }
29 | ],
30 | "idiom" : "universal",
31 | "scale" : "2x"
32 | },
33 | {
34 | "idiom" : "universal",
35 | "scale" : "3x"
36 | },
37 | {
38 | "appearances" : [
39 | {
40 | "appearance" : "luminosity",
41 | "value" : "dark"
42 | }
43 | ],
44 | "idiom" : "universal",
45 | "scale" : "3x"
46 | }
47 | ],
48 | "info" : {
49 | "author" : "xcode",
50 | "version" : 1
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/mentorship ios/en.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings
3 | Created on 04/06/20.
4 | Created for AnitaB.org Mentorship-iOS
5 | */
6 |
7 | "Terms and conditions" = "I confirm that I have read and accept to be bound by the AnitaB.org Code of Conduct, Terms, and Privacy Policy. Further, I consent to the use of my information for the stated purpose.";
8 |
9 | "About text" = "Systers is an international community for all women involved in the technical aspects of computing. We welcome the participation of women technologists of all ages and at any stage of their studies or careers.\n\nMentorship System is an application that matches women in tech to mentor each other, on career development, through 1:1 relations during a certain period of time. This is the iOS application of this project.";
10 |
11 | "Operation failed" = "Unable to complete request. Please try again.";
12 |
13 | "Report comment message" = "Reporting this comment will send an email to the AnitaB.org admins which will include the comment and identification of the commenter and you.";
14 |
15 | "Can be both" = "Available to be a Mentor or Mentee";
16 | "Can be mentee" = "Availabe to be a Mentee";
17 | "Can be mentor" = "Availabe to be a Mentor";
18 |
--------------------------------------------------------------------------------
/mentorship ios/ViewModels/LoginViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoginViewModel.swift
3 | // Created on 21/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 | import Combine
9 |
10 | class LoginViewModel: ObservableObject {
11 |
12 | // MARK: - Variables
13 | @Published var loginData = LoginModel.LoginUploadData(username: "", password: "")
14 | @Published var loginResponseData = LoginModel.LoginResponseData(message: "")
15 | @Published var inActivity = false
16 | private lazy var appleSignInCoordinator = AppleSignInCoordinator(loginVM: self)
17 |
18 | var loginDisabled: Bool {
19 | if self.loginData.username.isEmpty || self.loginData.password.isEmpty {
20 | return true
21 | }
22 | return false
23 | }
24 |
25 | // MARK: Functions
26 | func update(using data: LoginModel.LoginResponseData) {
27 | loginResponseData = data
28 | }
29 |
30 | func attemptAppleLogin() {
31 | appleSignInCoordinator.handleAuthorizationAppleIDButtonPress()
32 | }
33 |
34 | // used to reset the login data on a successful login
35 | func resetLogin() {
36 | self.loginData = .init(username: "", password: "")
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/mentorship ios/ViewModels/ChangePasswordViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChangePasswordViewModel.swift
3 | // Created on 26/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 | import Combine
9 |
10 | class ChangePasswordViewModel: ObservableObject {
11 |
12 | // MARK: - Variables
13 | @Published var changePasswordData = ChangePasswordModel.ChangePasswordUploadData(currentPassword: "", newPassword: "")
14 | @Published var changePasswordResponseData = ChangePasswordModel.ChangePasswordResponseData(message: "", success: false)
15 | @Published var confirmPassword: String = ""
16 | @Published var inActivity: Bool = false
17 |
18 | var changePasswordDisabled: Bool {
19 | if self.changePasswordData.newPassword.isEmpty || self.changePasswordData.newPassword.isEmpty || self.confirmPassword.isEmpty {
20 | return true
21 | }
22 | return false
23 | }
24 |
25 | func resetData(){
26 | self.changePasswordData = ChangePasswordModel.ChangePasswordUploadData(currentPassword: "", newPassword: "")
27 | self.changePasswordResponseData = ChangePasswordModel.ChangePasswordResponseData(message: "", success: false)
28 | self.confirmPassword = ""
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/mentorship ios/Views/Home/SupportingViews/TasksToDoSection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TasksToDoSection.swift
3 | // Created on 01/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct TasksToDoSection: View {
10 | var tasksToDo: [T]
11 | var onTapAction: () -> Void
12 |
13 | init(tasksToDo: [T]?, onTapAction: @escaping () -> Void = { }) {
14 | self.tasksToDo = tasksToDo ?? []
15 | self.onTapAction = onTapAction
16 | }
17 |
18 | var body: some View {
19 | Section(header: Text(LocalizableStringConstants.tasksToDo).font(.headline)) {
20 | ForEach(tasksToDo) { task in
21 | HStack {
22 | Image(systemName: ImageNameConstants.SFSymbolConstants.taskToDo)
23 | .foregroundColor(DesignConstants.Colors.defaultIndigoColor)
24 | .padding(.trailing, DesignConstants.Padding.insetListCellFrameExpansion)
25 |
26 | Text(task.description ?? "-")
27 | .font(.subheadline)
28 | }
29 | .onTapGesture {
30 | self.onTapAction()
31 | }
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/mentorship iosTests/MockURLProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockURLProtocol.swift
3 | // Created on 25/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 | import XCTest
9 |
10 | class MockURLProtocol: URLProtocol {
11 | static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
12 |
13 | override class func canInit(with request: URLRequest) -> Bool {
14 | return true
15 | }
16 |
17 | override class func canonicalRequest(for request: URLRequest) -> URLRequest {
18 | return request
19 | }
20 |
21 | override func startLoading() {
22 | guard let handler = MockURLProtocol.requestHandler else {
23 | XCTFail("Received unexpected request with no handler set")
24 | return
25 | }
26 | do {
27 | let (response, data) = try handler(request)
28 | client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
29 | client?.urlProtocol(self, didLoad: data)
30 | client?.urlProtocolDidFinishLoading(self)
31 | } catch {
32 | client?.urlProtocol(self, didFailWithError: error)
33 | }
34 | }
35 |
36 | override func stopLoading() {
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/mentorship ios/Models/MembersModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MembersModel.swift
3 | // Created on 07/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | final class MembersModel {
8 |
9 | // MARK: - Structures
10 | struct MembersResponseData: Encodable, Identifiable, MemberProperties {
11 | let id: Int
12 |
13 | let username: String?
14 | let name: String?
15 |
16 | let bio: String?
17 | let location: String?
18 | let occupation: String?
19 | let organization: String?
20 | let interests: String?
21 | let skills: String?
22 |
23 | let slackUsername: String?
24 | let needMentoring: Bool?
25 | let availableToMentor: Bool?
26 | let isAvailable: Bool?
27 | }
28 |
29 | struct SendRequestUploadData: Encodable {
30 | var mentorID: Int
31 | var menteeID: Int
32 | var endDate: Double
33 | var notes: String
34 |
35 | enum CodingKeys: String, CodingKey {
36 | case notes
37 | case mentorID = "mentor_id"
38 | case menteeID = "mentee_id"
39 | case endDate = "end_date"
40 | }
41 | }
42 |
43 | struct SendRequestResponseData: Encodable {
44 | let message: String?
45 | var success: Bool
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/mentorship ios/Models/RequestModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RequestModel.swift
3 | // Created on 24/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | struct RequestsList: Codable {
8 | let sent: RequestStructures?
9 | let received: RequestStructures?
10 |
11 | struct RequestStructures: Codable {
12 | let accepted: [RequestStructure]?
13 | let rejected: [RequestStructure]?
14 | let completed: [RequestStructure]?
15 | let cancelled: [RequestStructure]?
16 | let pending: [RequestStructure]?
17 | }
18 | }
19 |
20 | struct RequestStructure: Codable, Identifiable {
21 | let id: Int?
22 | let mentor: Info?
23 | let mentee: Info?
24 | let endDate: Double?
25 | let notes: String?
26 |
27 | enum CodingKeys: String, CodingKey {
28 | case id, mentor, mentee, notes
29 | case endDate = "end_date"
30 | }
31 |
32 | //info struct for mentor/mentee information
33 | struct Info: Codable {
34 | let id: Int?
35 | let userName: String?
36 | let name: String?
37 |
38 | enum CodingKeys: String, CodingKey {
39 | case id, name
40 | case userName = "user_name"
41 | }
42 | }
43 | }
44 |
45 | struct RequestActionResponse: Codable {
46 | let message: String?
47 | }
48 |
--------------------------------------------------------------------------------
/mentorship ios/Views/Members/SupportingViews/MembersListCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MembersListCell.swift
3 | // Created on 07/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | //Used in Members.swift for the list of members
10 | struct MembersListCell: View {
11 | var member: MembersModel.MembersResponseData
12 | var membersViewModel: MembersViewModel
13 | var body: some View {
14 | VStack(alignment: .leading, spacing: DesignConstants.Spacing.minimalSpacing) {
15 | //Name
16 | Text(member.name ?? "")
17 | .font(.headline)
18 |
19 | Group {
20 | //Availability: mentor and/or mentee
21 | Text(self.membersViewModel.availabilityString(canBeMentee: member.needMentoring ?? false, canBeMentor: member.availableToMentor ?? false))
22 |
23 | //Skills
24 | Text(self.membersViewModel.skillsString(skills: member.skills ?? ""))
25 | }
26 | .font(.subheadline)
27 | .foregroundColor(DesignConstants.Colors.subtitleText)
28 | }
29 | }
30 | }
31 |
32 | //struct MembersListCell_Previews: PreviewProvider {
33 | // static var previews: some View {
34 | // MembersListCell(member: MembersModel.MembersResponseData.self, membersModel: MembersModel.self)
35 | // }
36 | //}
37 |
--------------------------------------------------------------------------------
/.github/contributing_guidelines.md:
--------------------------------------------------------------------------------
1 | # Welcome to Mentorship iOS Project.
2 |
3 | We welcome anyone to contribute to this Open Source Project.
4 |
5 | ## Contribution Guidelines
6 |
7 | 1. Pick an open issue from the [issue list](https://github.com/anitab-org/mentorship-ios/issues) and claim it in the comments. After approval fix the issue and send us a pull request (PR).
8 | 2. Or you can create a new issue. A community member will get back to you and, if approved, you can fix the issue and send a pull request.
9 | 3. Please go through our issue list first (open as well as closed) and make sure the issue you are reporting does not replicate an issue already reported. If you have issues on multiple pages, report them separately. Do not combine them into a single issue.
10 | 4. All the PR’s need to be sent to the appropriate branch (usually "develop").
11 |
12 | ## Avoid the following mistakes!
13 |
14 | 1. Fix a new issue and submit a PR without reporting and getting it approved at first.
15 | 2. Fix an issue assigned to someone else and submit a PR before the assignee does.
16 | 3. Report issues which are previously reported by others. (Please check the both open and closed issues before you report an issue).
17 | 4. Suggest completely new features in the issue list. (Please use the mailing list/zulip channel for these kinds of suggestions. Use issue list to suggest bugs/features in the already implemented sections.
18 |
--------------------------------------------------------------------------------
/mentorship ios/Protocols/ProfileProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileProperties.swift
3 | // Created on 18/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | //protocols used for member and self profile properties
8 | //helps in reusing profile page for both - member and user
9 | //done by using protocol as type in ProfileCommonDetailsSection.swift (Under Views)
10 |
11 | protocol MemberProperties {
12 | var id: Int { get }
13 | var name: String? { get }
14 | var username: String? { get }
15 | var bio: String? { get }
16 | var location: String? { get }
17 | var occupation: String? { get }
18 | var organization: String? { get }
19 | var slackUsername: String? { get }
20 | var skills: String? { get }
21 | var interests: String? { get }
22 | var needMentoring: Bool? { get }
23 | var availableToMentor: Bool? { get }
24 | }
25 |
26 | protocol ProfileProperties: MemberProperties {
27 | var id: Int { get }
28 | var name: String? { get set }
29 | var username: String? { get set }
30 | var bio: String? { get set }
31 | var location: String? { get set }
32 | var occupation: String? { get set }
33 | var organization: String? { get set }
34 | var slackUsername: String? { get set }
35 | var skills: String? { get set }
36 | var interests: String? { get set }
37 | var needMentoring: Bool? { get set }
38 | var availableToMentor: Bool? { get set }
39 | }
40 |
--------------------------------------------------------------------------------
/mentorship ios/Utilities/UIHelper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIHelper.swift
3 | // Created on 21/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | class UIHelper {
10 | //helper for Home Screen relations list
11 | struct HomeScreen {
12 | struct RelationsListData {
13 | let relationTitle = [
14 | "Pending",
15 | "Accepted",
16 | "Rejected",
17 | "Cancelled",
18 | "Completed"
19 | ]
20 | let relationImageName = [
21 | ImageNameConstants.SFSymbolConstants.pending,
22 | ImageNameConstants.SFSymbolConstants.accepted,
23 | ImageNameConstants.SFSymbolConstants.rejected,
24 | ImageNameConstants.SFSymbolConstants.cancelled,
25 | ImageNameConstants.SFSymbolConstants.completed
26 | ]
27 | let relationImageColor: [Color] = [
28 | DesignConstants.Colors.pending,
29 | DesignConstants.Colors.accepted,
30 | DesignConstants.Colors.rejected,
31 | DesignConstants.Colors.cancelled,
32 | DesignConstants.Colors.defaultIndigoColor
33 | ]
34 | var relationCount = [0, 0, 0, 0, 0]
35 | }
36 | }
37 |
38 | //views for section 1 settings navigation destination
39 | let settingsViews: [AnyView] = [
40 | AnyView(About()), AnyView(Text("Feedback")), AnyView(ChangePassword())
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/mentorship ios/Managers/NetworkManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkManager.swift
3 | // Created on 05/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 | import Combine
9 |
10 | struct NetworkManager {
11 | static var responseCode: Int = 0
12 |
13 | static func callAPI(
14 | urlString: String,
15 | httpMethod: String = "GET",
16 | uploadData: Data = Data(),
17 | token: String = "",
18 | session: URLSession = .shared
19 | ) -> AnyPublisher {
20 | //set response code to 0
21 | responseCode = 0
22 |
23 | //convert url string to url
24 | let url = URL(string: urlString)!
25 |
26 | //setup url request
27 | var request = URLRequest(url: url)
28 | request.setValue("application/json", forHTTPHeaderField: "Content-Type")
29 | if !token.isEmpty {
30 | request.setValue(token, forHTTPHeaderField: "Authorization")
31 | }
32 | request.httpMethod = httpMethod
33 | request.httpBody = uploadData
34 |
35 | //make the call using the url request
36 | return session
37 | .dataTaskPublisher(for: request)
38 | .tryMap {
39 | if let response = $0.response as? HTTPURLResponse {
40 | self.responseCode = response.statusCode
41 | }
42 | return $0.data
43 | }
44 | .decode(type: T.self, decoder: JSONDecoder())
45 | .eraseToAnyPublisher()
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/mentorship ios/Models/RelationModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RelationModel.swift
3 | // Created on 02/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | class RelationModel {
8 |
9 | // MARK: - Variables
10 | let currentRelation = RequestStructure(id: 0, mentor: nil, mentee: nil, endDate: 0, notes: "")
11 |
12 | let tasks = [TaskStructure]()
13 |
14 | let task = TaskStructure(id: 0, description: "", isDone: false, createdAt: 0, completedAt: 0)
15 |
16 | // MARK: - Structures
17 | struct ResponseData: Encodable {
18 | var message: String?
19 | let success: Bool
20 | }
21 |
22 | struct AddTaskData: Encodable {
23 | var description: String
24 | }
25 | }
26 |
27 | // MARK: API
28 |
29 | extension RequestStructure {
30 | func update(viewModel: RelationViewModel) {
31 | viewModel.currentRelation = self
32 | //if current relation invalid, delete all tasks and return
33 | if self.id == nil {
34 | viewModel.toDoTasks.removeAll()
35 | viewModel.doneTasks.removeAll()
36 | }
37 | }
38 | }
39 |
40 | extension RelationModel.ResponseData {
41 | func update(viewModel: RelationViewModel) {
42 | viewModel.responseData = self
43 | }
44 | }
45 |
46 | extension TaskStructure {
47 | func update(viewModel: RelationViewModel) {
48 | if self.isDone ?? false {
49 | viewModel.doneTasks.append(self)
50 | } else {
51 | viewModel.toDoTasks.append(self)
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Docs/Configuring Remotes.md:
--------------------------------------------------------------------------------
1 | # Configure Remotes
2 | When a repository is cloned, it has a default remote called `origin` that points to your fork on GitHub, not the original repository it was forked from. To keep track of the original repository, you should add another remote named `upstream`:
3 | 1. Get the path where you have your git repository on your machine. Go to that path in Terminal using cd. Alternatively, right click on project in Github Desktop and hit ‘Open in Terminal’.
4 | 2. Run `git remote -v` to check the status you should see something like the following:
5 | > origin https://github.com/YOUR_USERNAME/mentorship-ios.git (fetch)
6 | > origin https://github.com/YOUR_USERNAME/mentorship-ios.git (push)
7 | 3. Set the `upstream`:
8 | `git remote add upstream https://github.com/anitab-org/mentorship-ios.git`
9 | 4. Run `git remote -v` again to check the status, you should see something like the following:
10 | > origin https://github.com/YOUR_USERNAME/mentorship-ios.git (fetch)
11 | > origin https://github.com/YOUR_USERNAME/mentorship-ios.git (push)
12 | > upstream https://github.com/anitab-org/mentorship-ios.git (fetch)
13 | > upstream https://github.com/anitab-org/mentorship-ios.git (push)
14 | 5. To update your local copy with remote changes, run the following:
15 | `git fetch upstream develop`
16 | `git rebase upstream/develop`
17 | This will give you an exact copy of the current remote, make sure you don't have any local changes.
18 | 6. Project set-up is complete.
19 |
--------------------------------------------------------------------------------
/mentorship ios/Models/ProfileModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileModel.swift
3 | // Created on 12/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | final class ProfileModel {
8 |
9 | // MARK: - Structures
10 | struct ProfileData: Codable, Equatable, ProfileProperties {
11 | let id: Int
12 | var name: String?
13 | var username: String?
14 | let email: String?
15 | var bio: String?
16 | var location: String?
17 | var occupation: String?
18 | var organization: String?
19 | var slackUsername: String?
20 | var skills: String?
21 | var interests: String?
22 | var needMentoring: Bool?
23 | var availableToMentor: Bool?
24 |
25 | enum CodingKeys: String, CodingKey {
26 | case id, name, username, email, bio, location, occupation, organization, skills, interests
27 | case slackUsername = "slack_username"
28 | case needMentoring = "need_mentoring"
29 | case availableToMentor = "available_to_mentor"
30 | }
31 | }
32 |
33 | struct UpdateProfileResponseData: Encodable {
34 | let success: Bool?
35 | let message: String?
36 | }
37 |
38 | }
39 |
40 | // MARK: - API
41 | extension ProfileModel.ProfileData {
42 | func update(viewModel: HomeViewModel) {
43 | viewModel.userName = self.name
44 | }
45 | }
46 |
47 | extension ProfileModel.UpdateProfileResponseData {
48 | func update(viewModel: ProfileViewModel) {
49 | viewModel.updateProfileResponseData = self
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/mentorship ios/Views/UtilityViews/CommonProfileSection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileDetailCellsGroup.swift
3 | // Created on 18/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct ProfileCommonDetailsSection: View {
10 | var memberData: MemberProperties
11 | var hideEmptyFields: Bool
12 |
13 | var body: some View {
14 | Section {
15 | MemberDetailCell(type: .username, value: memberData.username, hideEmptyFields: hideEmptyFields)
16 | MemberDetailCell(type: .isMentor, value: memberData.availableToMentor ?? false ? "Yes" : "No", hideEmptyFields: hideEmptyFields)
17 | MemberDetailCell(type: .needsMentor, value: memberData.needMentoring ?? false ? "Yes" : "No", hideEmptyFields: hideEmptyFields)
18 | MemberDetailCell(type: .interests, value: memberData.interests, hideEmptyFields: hideEmptyFields)
19 | MemberDetailCell(type: .bio, value: memberData.bio, hideEmptyFields: hideEmptyFields)
20 | MemberDetailCell(type: .location, value: memberData.location, hideEmptyFields: hideEmptyFields)
21 | MemberDetailCell(type: .occupation, value: memberData.occupation, hideEmptyFields: hideEmptyFields)
22 | MemberDetailCell(type: .organization, value: memberData.organization, hideEmptyFields: hideEmptyFields)
23 | MemberDetailCell(type: .skills, value: memberData.skills, hideEmptyFields: hideEmptyFields)
24 | MemberDetailCell(type: .slackUsername, value: memberData.slackUsername, hideEmptyFields: hideEmptyFields)
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/mentorship ios/Utility/DesignConstants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DesignConstants.swift
3 | // Created on 04/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct DesignConstants {
10 |
11 | struct Spacing {
12 | static let bigSpacing: CGFloat = 48
13 | static let smallSpacing: CGFloat = 16
14 | static let minimalSpacing: CGFloat = 6
15 | }
16 |
17 | struct Screen {
18 | struct Padding {
19 | // default SwiftUI padding value = 16
20 | static let topPadding: CGFloat = 16
21 | static let bottomPadding: CGFloat = 16
22 | static let leadingPadding: CGFloat = 16
23 | static let trailingPadding: CGFloat = 16
24 | }
25 | }
26 |
27 | struct Form {
28 | struct Spacing {
29 | static let bigSpacing: CGFloat = 46
30 | static let smallSpacing: CGFloat = 16
31 | static let minimalSpacing: CGFloat = 6
32 | }
33 | }
34 |
35 | struct Padding {
36 | //used to expand frame, eg. of textfield
37 | static let frameExpansionPadding: CGFloat = 10
38 | }
39 |
40 | struct CornerRadius {
41 | static let preferredCornerRadius: CGFloat = 5
42 | }
43 |
44 | struct Opacity {
45 | static let disabledViewOpacity: Double = 0.75
46 | static let tapHighlightingOpacity: Double = 0.75
47 | }
48 |
49 | struct Colors {
50 | static let defaultIndigoColor = Color(.systemIndigo)
51 | static let secondaryBackground = Color(.secondarySystemBackground)
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/mentorship iosUITests/MentorshipUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // mentorship_iosUITests.swift
3 | // Created on 30/05/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import XCTest
8 |
9 | class MentorshipUITests: XCTestCase {
10 |
11 | override func setUpWithError() throws {
12 | // Put setup code here. This method is called before the invocation of each test method in the class.
13 |
14 | // In UI tests it is usually best to stop immediately when a failure occurs.
15 | continueAfterFailure = false
16 |
17 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
18 | }
19 |
20 | override func tearDownWithError() throws {
21 | // Put teardown code here. This method is called after the invocation of each test method in the class.
22 | }
23 |
24 | func testExample() throws {
25 | // UI tests must launch the application that they test.
26 | let app = XCUIApplication()
27 | app.launch()
28 |
29 | // Use recording to get started writing UI tests.
30 | // Use XCTAssert and related functions to verify your tests produce the correct results.
31 | }
32 |
33 | func testLaunchPerformance() throws {
34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) {
35 | // This measures how long it takes to launch your application.
36 | measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) {
37 | XCUIApplication().launch()
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Description
2 | Include a summary of the change and relevant motivation/context. List any dependencies that are required for this change.
3 |
4 | Fixes # [ISSUE]
5 |
6 | ### Type of Change:
7 | **Delete irrelevant options.**
8 |
9 | - Code
10 | - Quality Assurance
11 | - User Interface
12 | - Outreach
13 | - Documentation
14 |
15 | **Code/Quality Assurance Only**
16 | - Bug fix (non-breaking change which fixes an issue)
17 | - This change requires a documentation update (software upgrade on readme file)
18 | - New feature (non-breaking change which adds functionality pre-approved by mentors)
19 |
20 |
21 |
22 | ### How Has This Been Tested?
23 | Describe the tests you ran to verify your changes. Provide instructions or GIFs so we can reproduce. List any relevant details for your test.
24 |
25 |
26 | ### Checklist:
27 | **Delete irrelevant options.**
28 |
29 | - [ ] My PR follows the style guidelines of this project
30 | - [ ] I have performed a self-review of my own code or materials
31 | - [ ] I have commented my code or provided relevant documentation, particularly in hard-to-understand areas
32 | - [ ] I have made corresponding changes to the documentation
33 | - [ ] Any dependent changes have been merged
34 |
35 | **Code/Quality Assurance Only**
36 | - [ ] My changes generate no new warnings
37 | - [ ] My PR currently breaks something (fix or feature that would cause existing functionality to not work as expected)
38 | - [ ] I have added tests that prove my fix is effective or that my feature works
39 | - [ ] New and existing unit tests pass locally with my changes
40 | - [ ] Any dependent changes have been published in downstream modules
41 |
--------------------------------------------------------------------------------
/mentorship ios/Views/Profile/ProfileSummary.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Profile.swift
3 | // Created on 18/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct ProfileSummary: View {
10 | @ObservedObject var profileViewModel = ProfileViewModel()
11 | var profileData: ProfileModel.ProfileData {
12 | return profileViewModel.getProfile()
13 | }
14 | @State private var showProfileEditor = false
15 |
16 | var body: some View {
17 | List {
18 | //By using a parent section, the subsections join.
19 | Section {
20 | //Name
21 | MemberDetailCell(type: .name, value: profileData.name ?? "-", hideEmptyFields: false)
22 |
23 | //Show common details : name, username, occupation, etc.
24 | ProfileCommonDetailsSection(memberData: profileData, hideEmptyFields: false)
25 | }
26 |
27 | //Show email
28 | Text(profileData.email ?? "")
29 | .font(.subheadline)
30 | .listRowBackground(DesignConstants.Colors.formBackgroundColor)
31 | }
32 | .listStyle(GroupedListStyle())
33 | .navigationBarTitle(LocalizableStringConstants.profile)
34 | .navigationBarItems(trailing:
35 | Button("Edit") {
36 | self.showProfileEditor.toggle()
37 | })
38 | .sheet(isPresented: $showProfileEditor) {
39 | ProfileEditor()
40 | }
41 | }
42 | }
43 |
44 | struct Profile_Previews: PreviewProvider {
45 | static var previews: some View {
46 | ProfileSummary()
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/mentorship ios/Constants/ImageNameConstants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageNames.swift
3 | // Created on 04/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | struct ImageNameConstants {
8 | static let mentorshipLogoImageName = "mentorship_system_logo"
9 |
10 | struct SFSymbolConstants {
11 | //tab icons
12 | static let home = "house.fill"
13 | static let relation = "command"
14 | static let members = "person.3.fill"
15 | static let settings = "gear"
16 |
17 | //relations list icons (on home screen)
18 | static let pending = "arrow.2.circlepath.circle.fill"
19 | static let accepted = "checkmark.circle.fill"
20 | static let rejected = "xmark.circle.fill"
21 | static let cancelled = "trash.circle.fill"
22 | static let completed = "archivebox.fill"
23 |
24 | //icons used on home screen
25 | static let taskDone = "checkmark"
26 | static let taskToDo = "circle"
27 | static let profileIcon = "person.crop.circle.fill"
28 |
29 | //settings icons
30 | static let about = "info.circle"
31 | static let feedback = "square.and.pencil"
32 | static let changePassword = "lock"
33 | static let logout = "power"
34 | static let deleteAccount = "trash"
35 |
36 | //dismiss sheet button icon
37 | static let xCircle = "x.circle.fill"
38 |
39 | // report violation icon
40 | static let reportComment = "exclamationmark.bubble"
41 |
42 | //ellipsis icon ("..."). used to show additional controls
43 | static let ellipsis = "ellipsis"
44 |
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/mentorship ios/Views/Settings/IndividualSetting/About.swift:
--------------------------------------------------------------------------------
1 | //
2 | // About.swift
3 | // Created on 25/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct About: View {
10 | var body: some View {
11 | ScrollView {
12 | VStack(spacing: DesignConstants.Spacing.bigSpacing) {
13 | //logo image
14 | Image(ImageNameConstants.mentorshipLogoImageName)
15 | .resizable()
16 | .scaledToFit()
17 |
18 | //about text
19 | Text(LocalizableStringConstants.aboutText)
20 | .font(.body)
21 | .fixedSize(horizontal: false, vertical: true)
22 |
23 | //Links to privacy policy and terms of use.
24 | HStack(spacing: DesignConstants.Spacing.bigSpacing) {
25 | //privacy policy
26 | NavigationLink(
27 | LocalizableStringConstants.privacyPolicy,
28 | destination: WebView(urlString: URLStringConstants.WebsiteURLs.privacyPolicy))
29 |
30 | //terms of use
31 | NavigationLink(
32 | LocalizableStringConstants.termsOfUse,
33 | destination: WebView(urlString: URLStringConstants.WebsiteURLs.termsOfUse))
34 | }
35 | }
36 | }
37 | .padding(.horizontal)
38 | .navigationBarTitle("About", displayMode: .inline)
39 | }
40 | }
41 |
42 | struct About_Previews: PreviewProvider {
43 | static var previews: some View {
44 | About()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/mentorship ios/Views/Members/MemberDetail.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MemberDetail.swift
3 | // Created on 08/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct MemberDetail: View {
10 | var memberData: MemberProperties
11 | @State private var showSendRequestSheet = false
12 | let hideEmptyFields = true
13 |
14 | var body: some View {
15 | List {
16 | ProfileCommonDetailsSection(memberData: memberData, hideEmptyFields: hideEmptyFields)
17 | }
18 | .navigationBarTitle(memberData.name ?? "memberData Detail")
19 | .navigationBarItems(trailing:
20 | Button(action: { self.showSendRequestSheet.toggle() }) {
21 | Text("Send Request")
22 | .font(.headline)
23 | })
24 | .sheet(isPresented: $showSendRequestSheet) {
25 | SendRequest(memberID: self.memberData.id, memberName: self.memberData.name ?? "-")
26 | }
27 | }
28 | }
29 |
30 | struct MemberDetail_Previews: PreviewProvider {
31 | static var previews: some View {
32 | MemberDetail(
33 | memberData: MembersModel.MembersResponseData.init(
34 | id: 1,
35 | username: "username",
36 | name: "yugantar",
37 | bio: "student",
38 | location: "earth",
39 | occupation: "student",
40 | organization: "",
41 | interests: "astronomy",
42 | skills: "ios, swift, c++",
43 | slackUsername: "",
44 | needMentoring: true,
45 | availableToMentor: true,
46 | isAvailable: true
47 | )
48 | )
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/mentorship ios/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/mentorship iosTests/MentorshipTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // mentorship_iosTests.swift
3 | // Created on 30/05/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import XCTest
8 | import Combine
9 | @testable import mentorship_ios
10 |
11 | class MentorshipTests: XCTestCase {
12 | var urlSession: URLSession!
13 |
14 | override func setUpWithError() throws {
15 | // Set url session for mock networking
16 | let configuration = URLSessionConfiguration.ephemeral
17 | configuration.protocolClasses = [MockURLProtocol.self]
18 | urlSession = URLSession(configuration: configuration)
19 | }
20 |
21 | override func tearDownWithError() throws {
22 | // Put teardown code here. This method is called after the invocation of each test method in the class.
23 | urlSession = nil
24 | super.tearDown()
25 | }
26 |
27 | //this test requires a stable internet connection
28 | func testBackendServerURL() {
29 |
30 | // Create an expectation for a background download task.
31 | let expectation = XCTestExpectation(description: "Download mentorship backend base url")
32 |
33 | // Create a URL for a web page to be downloaded.
34 | let url = URL(string: baseURL)!
35 |
36 | // Create a background task to download the web page.
37 | let cancellable: AnyCancellable?
38 | cancellable = URLSession.shared.dataTaskPublisher(for: url)
39 | .assertNoFailure()
40 | .sink {
41 | // Make sure we downloaded some data.
42 | XCTAssertNotNil($0, "No data was downloaded.")
43 |
44 | // Fulfill the expectation to indicate that the background task has finished successfully.
45 | expectation.fulfill()
46 | }
47 |
48 | // Wait until the expectation is fulfilled, with a timeout of 10 seconds.
49 | wait(for: [expectation], timeout: 10.0)
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/mentorship ios/Service/Networking/RequestActionAPI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RelationRequestActions.swift
3 | // Created on 07/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 | import Combine
9 |
10 | enum ActionType {
11 | case accept, reject, delete //for pending requests
12 | case cancel //for accepted request
13 | }
14 |
15 | class RequestActionAPI: RequestActionService {
16 | private var cancellable: AnyCancellable?
17 | let urlSession: URLSession
18 |
19 | init(urlSession: URLSession = .shared) {
20 | self.urlSession = urlSession
21 | }
22 |
23 | func actOnPendingRequest(
24 | action: ActionType,
25 | reqID: Int,
26 | completion: @escaping (RequestActionResponse, Bool) -> Void
27 | ) {
28 | var urlString = ""
29 | var httpMethod = "PUT"
30 |
31 | //set url string
32 | switch action {
33 | case .accept: urlString = URLStringConstants.MentorshipRelation.accept(reqID: reqID)
34 | case .reject: urlString = URLStringConstants.MentorshipRelation.reject(reqID: reqID)
35 | case .delete:
36 | urlString = URLStringConstants.MentorshipRelation.delete(reqID: reqID)
37 | httpMethod = "DELETE"
38 | case .cancel: urlString = URLStringConstants.MentorshipRelation.cancel(reqID: reqID)
39 | }
40 |
41 | //get token
42 | guard let token = try? KeychainManager.getToken() else {
43 | return
44 | }
45 |
46 | //api call
47 | cancellable = NetworkManager.callAPI(urlString: urlString, httpMethod: httpMethod, token: token, session: urlSession)
48 | .receive(on: RunLoop.main)
49 | .catch { _ in Just(RequestActionResponse(message: LocalizableStringConstants.networkErrorString)) }
50 | .sink {
51 | let success = NetworkManager.responseCode == 200
52 | completion($0, success)
53 | }
54 | }
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/Docs/Contributing and Developing.md:
--------------------------------------------------------------------------------
1 | # Contributing and developing a feature
2 | 1. Make sure you are in the develop branch `git checkout develop`.
3 | 2. Sync your copy `git pull --rebase upstream develop`.
4 | 3. Create a new branch with a meaningful name `git checkout -b branch_name`.
5 | 4. Develop your feature on Xcode IDE and run it using the simulator or connecting your own iphone.
6 | 5. Add the files you changed `git add file_name` (avoid using `git add .`).
7 | 6. Commit your changes `git commit -m "Message briefly explaining the feature"`.
8 | 7. Keep one commit per feature. If you forgot to add changes, you can edit the previous commit `git commit --amend`.
9 | 8. Push to your repo `git push origin branch-name`.
10 | 9. Go into [the Github repo](https://github.com/anitab-org/powerup-iOS/) and create a pull request explaining your changes.
11 | 10. If you are requested to make changes, edit your commit using `git commit --amend`, push again and the pull request will edit automatically.
12 | 11. If you have more than one commit try squashing them into single commit by following command:
13 | `git rebase -i origin/master~n master`(having n number of commits).
14 | 12. Once you've run a git rebase -i command, text editor will open with a file that lists all the commits in current branch, and in front of each commit is the word "pick". For every line except the first, replace the word "pick" with the word "squash".
15 | 13. Save and close the file, and a moment later a new file should pop up in editor, combining all the commit messages of all the commits. Reword this commit message into meaningful one briefly explaining all the features, and then save and close that file as well. This commit message will be the commit message for the one, big commit that you are squashing all of your larger commits into. Once you've saved and closed that file, your commits of current branch have been squashed together.
16 | 14. Force push to update your pull request with command `git push origin branchname --force`.
17 |
--------------------------------------------------------------------------------
/mentorship ios/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/mentorship ios/TabBar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabBar.swift
3 | // Created on 08/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct TabBar: View {
10 | @Binding var selection: Int
11 |
12 | var body: some View {
13 | TabView(selection: $selection) {
14 | //Home
15 | Home()
16 | .tabItem {
17 | VStack {
18 | Image(systemName: ImageNameConstants.SFSymbolConstants.home)
19 | .imageScale(.large)
20 | Text(LocalizableStringConstants.ScreenNames.home)
21 | }
22 | }.tag(0)
23 |
24 | //Relation
25 | Relation()
26 | .tabItem {
27 | VStack {
28 | Image(systemName: ImageNameConstants.SFSymbolConstants.relation)
29 | .imageScale(.large)
30 | Text(LocalizableStringConstants.ScreenNames.relation)
31 | }
32 | }.tag(1)
33 |
34 | //Members
35 | Members()
36 | .tabItem {
37 | VStack {
38 | Image(systemName: ImageNameConstants.SFSymbolConstants.members)
39 | .imageScale(.large)
40 | Text(LocalizableStringConstants.ScreenNames.members)
41 | }
42 | }.tag(2)
43 |
44 | //Settings
45 | Settings()
46 | .tabItem {
47 | VStack {
48 | Image(systemName: ImageNameConstants.SFSymbolConstants.settings)
49 | .imageScale(.large)
50 | Text(LocalizableStringConstants.ScreenNames.settings)
51 | }
52 | }.tag(3)
53 | }
54 | }
55 | }
56 |
57 | struct TabBar_Previews: PreviewProvider {
58 | static var previews: some View {
59 | TabBar(selection: .constant(1))
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/mentorship ios/Service/Networking/SignUpAPI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SignUpAPI.swift
3 | // Created on 23/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 | import Combine
9 |
10 | class SignUpAPI: SignUpService {
11 | private var cancellable: AnyCancellable?
12 | let urlSession: URLSession
13 |
14 | init(urlSession: URLSession = .shared) {
15 | self.urlSession = urlSession
16 | }
17 |
18 | func signUp(
19 | availabilityPickerSelection: Int,
20 | signUpData: SignUpModel.SignUpUploadData,
21 | confirmPassword: String,
22 | completion: @escaping (SignUpModel.SignUpResponseData) -> Void
23 | ) {
24 | // make variable for sign up data
25 | var signUpData = signUpData
26 |
27 | //assign availability values as per picker selection
28 | signUpData.needMentoring = availabilityPickerSelection > 1
29 | signUpData.availableToMentor = availabilityPickerSelection != 2
30 |
31 | //check password fields
32 | if signUpData.password != confirmPassword {
33 | let signUpResponseData = SignUpModel.SignUpResponseData(message: LocalizableStringConstants.passwordsDoNotMatch)
34 | completion(signUpResponseData)
35 | return
36 | }
37 |
38 | //encode upload data
39 | guard let uploadData = try? JSONEncoder().encode(signUpData) else {
40 | return
41 | }
42 |
43 | //api call
44 | cancellable = NetworkManager.callAPI(urlString: URLStringConstants.Users.signUp, httpMethod: "POST", uploadData: uploadData, session: urlSession)
45 | .receive(on: RunLoop.main)
46 | .catch { _ in Just(SignUpNetworkModel(message: LocalizableStringConstants.networkErrorString)) }
47 | .sink {
48 | let signUpResponseData = SignUpModel.SignUpResponseData(message: $0.message)
49 | completion(signUpResponseData)
50 | }
51 | }
52 |
53 | struct SignUpNetworkModel: Decodable {
54 | var message: String?
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/mentorship ios/Service/Networking/HomeAPI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeAPI.swift
3 | // Created on 22/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 | import Combine
9 |
10 | class HomeAPI: HomeService {
11 | private var cancellable: AnyCancellable?
12 | let urlSession: URLSession
13 | // local variable. Helpful in state preservation (used in catch operator)
14 | var homeNetworkModel = HomeNetworkModel(asMentor: nil, asMentee: nil, tasksToDo: nil, tasksDone: nil)
15 |
16 | init(urlSession: URLSession = .shared) {
17 | self.urlSession = urlSession
18 | }
19 |
20 | func fetchDashboard(completion: @escaping (HomeModel.HomeResponseData) -> Void) {
21 | //get auth token
22 | guard let token = try? KeychainManager.getToken() else {
23 | return
24 | }
25 |
26 | //api call
27 | cancellable = NetworkManager.callAPI(urlString: URLStringConstants.Users.home, token: token, session: urlSession)
28 | .receive(on: RunLoop.main)
29 | .catch { _ in Just(self.homeNetworkModel) }
30 | .sink { home in
31 | // update home network model local variable.
32 | self.homeNetworkModel = home
33 | let homeResponse = HomeModel.HomeResponseData(
34 | asMentor: home.asMentor,
35 | asMentee: home.asMentee,
36 | tasksToDo: home.tasksToDo,
37 | tasksDone: home.tasksDone)
38 | // completion handler
39 | completion(homeResponse)
40 | }
41 | }
42 |
43 | struct HomeNetworkModel: Decodable {
44 | let asMentor: RequestsList?
45 | let asMentee: RequestsList?
46 |
47 | let tasksToDo: [TaskStructure]?
48 | let tasksDone: [TaskStructure]?
49 |
50 | enum CodingKeys: String, CodingKey {
51 | case asMentor = "as_mentor"
52 | case asMentee = "as_mentee"
53 | case tasksToDo = "tasks_todo"
54 | case tasksDone = "tasks_done"
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/mentorship ios/ViewModels/MembersViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MemberViewModel.swift
3 | // Created on 21/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 | import Combine
9 |
10 | final class MembersViewModel: ObservableObject {
11 |
12 | // MARK: - Variables
13 | @Published var membersResponseData = [MembersModel.MembersResponseData]()
14 | @Published var sendRequestResponseData = MembersModel.SendRequestResponseData(message: "", success: false)
15 | @Published var inActivity = false
16 | @Published var searchString = ""
17 |
18 | //used in pagination for members list
19 | let perPage = 20
20 | var currentPage = 0
21 | var membersListFull = false
22 |
23 | var currentlySearching = false
24 |
25 | // to backup original data, restore back after search completes
26 | var tempCurrentPage = 0
27 | var tempMembersListFull = false
28 | var tempMembersResponse = [MembersModel.MembersResponseData]()
29 |
30 | // MARK: - Functions
31 |
32 | func availabilityString(canBeMentee: Bool, canBeMentor: Bool) -> LocalizedStringKey {
33 | if canBeMentee && canBeMentor {
34 | return LocalizableStringConstants.canBeBoth
35 | } else if canBeMentee {
36 | return LocalizableStringConstants.canBeMentee
37 | } else if canBeMentor {
38 | return LocalizableStringConstants.canBeMentor
39 | } else {
40 | return LocalizableStringConstants.notAvailable
41 | }
42 | }
43 |
44 | func skillsString(skills: String) -> String {
45 | return "Skills: \(skills)"
46 | }
47 |
48 | // purpose: save original data (non-filtered by search)
49 | func backup() {
50 | tempCurrentPage = currentPage
51 | tempMembersListFull = membersListFull
52 | tempMembersResponse = membersResponseData
53 | }
54 |
55 | // restore backed up data after cancel button pressed in search bar
56 | func restore() {
57 | currentPage = tempCurrentPage
58 | membersListFull = tempMembersListFull
59 | membersResponseData = tempMembersResponse
60 | currentlySearching = false
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/mentorship ios/Views/Home/RelationDetailList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RelationDetailList.swift
3 | // Created on 16/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct RelationDetailList: View {
10 | var index: Int
11 | var navigationTitle: String
12 | var homeViewModel: HomeViewModel
13 | @State private var pickerSelection = 1
14 |
15 | var sentData: [RequestStructure]? {
16 | if pickerSelection == 1 {
17 | return homeViewModel.getSentDetailListData(userType: .mentee, index: index)
18 | } else {
19 | return homeViewModel.getSentDetailListData(userType: .mentor, index: index)
20 | }
21 | }
22 |
23 | var receivedData: [RequestStructure]? {
24 | if pickerSelection == 1 {
25 | return homeViewModel.getReceivedDetailListData(userType: .mentee, index: index)
26 | } else {
27 | return homeViewModel.getReceivedDetailListData(userType: .mentor, index: index)
28 | }
29 | }
30 |
31 | var body: some View {
32 | VStack {
33 | Picker(selection: $pickerSelection, label: Text("")) {
34 | Text("As Mentee").tag(1)
35 | Text("As Mentor").tag(2)
36 | }
37 | .pickerStyle(SegmentedPickerStyle())
38 | .labelsHidden()
39 | .padding()
40 |
41 | List {
42 | //received data list
43 | Section(header: Text("Received").font(.headline)) {
44 | ForEach(receivedData ?? []) { data in
45 | DetailListCell(cellVM: DetailListCellViewModel(data: data), index: self.index)
46 | }
47 | }
48 |
49 | //sent data list
50 | Section(header: Text("Sent").font(.headline)) {
51 | ForEach(sentData ?? []) { data in
52 | DetailListCell(cellVM: DetailListCellViewModel(data: data), index: self.index, sent: true)
53 | }
54 | }
55 | }
56 | }
57 | .navigationBarTitle(navigationTitle)
58 | }
59 | }
60 |
61 | struct RelationDetailList_Previews: PreviewProvider {
62 | static var previews: some View {
63 | RelationDetailList(index: 0, navigationTitle: "", homeViewModel: HomeViewModel())
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/.github/config.yml:
--------------------------------------------------------------------------------
1 | # Configuration for welcome - https://github.com/behaviorbot/welcome
2 |
3 | # Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome
4 |
5 | # Comment to be posted to on first time issues
6 |
7 | newIssueWelcomeComment: >
8 | Hello there!👋 Welcome to the project!💖
9 |
10 |
11 | Thank you and congrats🎉for opening your very first issue in this project. AnitaB.org is an inclusive community, committed to creating a safe and positive environment.🌸 Please adhere to our [Code of Conduct](https://github.com/anitab-org/mentorship-ios/blob/develop/.github/code_of_conduct.md).🙌
12 | You may submit a PR if you like! If you want to report a bug🐞 please follow our [Issue Template](https://github.com/anitab-org/mentorship-ios/tree/develop/.github/ISSUE_TEMPLATE). Also make sure you include steps to reproduce it and be patient while we get back to you.😄
13 |
14 |
15 | Feel free to join us on [AnitaB.org Open Source Zulip Community](https://anitab-org.zulipchat.com/).💖 We have different streams for each active repository for discussions.✨ Hope you have a great time there!😄
16 |
17 |
18 | # Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome
19 |
20 | # Comment to be posted to on PRs from first time contributors in your repository
21 |
22 | newPRWelcomeComment: >
23 | Hello there!👋 Welcome to the project!💖
24 |
25 |
26 | Thank you and congrats🎉 for opening your first pull request.✨ AnitaB.org is an inclusive community, committed to creating a safe and positive environment.🌸Please adhere to our [Code of Conduct](https://github.com/anitab-org/mentorship-ios/blob/develop/.github/code_of_conduct.md) and [Contributing Guidelines](https://github.com/anitab-org/mentorship-ios/blob/develop/.github/contributing_guidelines.md).🙌.We will get back to you as soon as we can.😄
27 |
28 |
29 | Feel free to join us on [AnitaB.org Open Source Zulip Community](https://anitab-org.zulipchat.com/).💖 We have different streams for each active repository for discussions.✨ Hope you have a great time there!😄
30 |
31 |
32 | # Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge
33 |
34 | # Comment to be posted to on pull requests merged by a first time user
35 |
36 | firstPRMergeComment: >
37 | Congrats on merging your first pull request! 🎉🎉🎉 We here at AnitaB.org are proud of you!
38 |
--------------------------------------------------------------------------------
/Docs/Screenshots.md:
--------------------------------------------------------------------------------
1 | ## Screenshots
2 | ||||
3 | |-|-|-|
4 | |Login|SignUp|Home|
5 | ||||
6 | |Request Detail|Profile|Profile Editor|
7 | ||||
8 | |Relation|Add Task|Mark as Complete|
9 | ||||
10 | |Task Comments|Report Comment|Members List|
11 | ||||
12 | |Members Search|Member Detail|Send Request|
13 | ||||
14 | |Settings|About|Change Password|
15 | ||||
16 |
--------------------------------------------------------------------------------
/mentorship ios/Service/Networking/ProfileAPI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileAPI.swift
3 | // Created on 22/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 | import Combine
9 |
10 | class ProfileAPI: ProfileService {
11 | private var cancellable: AnyCancellable?
12 | let urlSession: URLSession
13 |
14 | init(urlSession: URLSession = .shared) {
15 | self.urlSession = urlSession
16 | }
17 |
18 | // get user profile from backend
19 | func getProfile(completion: @escaping (ProfileModel.ProfileData) -> Void) {
20 | //get auth token
21 | guard let token = try? KeychainManager.getToken() else {
22 | return
23 | }
24 | print(token)
25 |
26 | //parallel request for profile and home
27 | cancellable = NetworkManager.callAPI(urlString: URLStringConstants.Users.user, token: token, session: urlSession)
28 | .receive(on: RunLoop.main)
29 | .catch { _ in Just(ProfileViewModel().getProfile()) }
30 | .sink { profile in
31 | ProfileViewModel().saveProfile(profile: profile)
32 | completion(profile)
33 | }
34 | }
35 |
36 | // makes api call to update profile
37 | func updateProfile(
38 | updateProfileData: ProfileModel.ProfileData,
39 | completion: @escaping (ProfileModel.UpdateProfileResponseData) -> Void
40 | ) {
41 | //get auth token
42 | guard let token = try? KeychainManager.getToken() else {
43 | return
44 | }
45 |
46 | //encoded upload data
47 | guard let uploadData = try? JSONEncoder().encode(updateProfileData) else {
48 | return
49 | }
50 |
51 | //api call
52 | cancellable = NetworkManager.callAPI(urlString: URLStringConstants.Users.user, httpMethod: "PUT", uploadData: uploadData, token: token, session: urlSession)
53 | .receive(on: RunLoop.main)
54 | .catch { _ in Just(UpdateProfileNetworkModel(message: LocalizableStringConstants.networkErrorString)) }
55 | .sink {
56 | let success = NetworkManager.responseCode == 200
57 | let profileResponse = ProfileModel.UpdateProfileResponseData(success: success, message: $0.message)
58 | completion(profileResponse)
59 | }
60 | }
61 |
62 | struct UpdateProfileNetworkModel: Decodable {
63 | let message: String?
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/mentorship ios/Models/HomeModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeModel.swift
3 | // Created on 12/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | class HomeModel {
8 | // MARK: - Structures
9 | struct HomeResponseData: Codable {
10 | let asMentor: RequestsList?
11 | let asMentee: RequestsList?
12 |
13 | let tasksToDo: [TaskStructure]?
14 | let tasksDone: [TaskStructure]?
15 | }
16 |
17 | enum UserType {
18 | case mentee, mentor
19 | }
20 | }
21 |
22 | // MARK: - API
23 | extension HomeModel.HomeResponseData {
24 | func update(viewModel: HomeViewModel) {
25 | viewModel.relationsListData.relationCount = updateCount(homeData: self)
26 | viewModel.homeResponseData = self
27 | }
28 |
29 | func updateCount(homeData: Self) -> [Int] {
30 | var pendingCount = homeData.asMentee?.sent?.pending?.count ?? 0
31 | pendingCount += homeData.asMentee?.received?.pending?.count ?? 0
32 | pendingCount += homeData.asMentor?.sent?.pending?.count ?? 0
33 | pendingCount += homeData.asMentor?.received?.pending?.count ?? 0
34 |
35 | var acceptedCount = homeData.asMentee?.sent?.accepted?.count ?? 0
36 | acceptedCount += homeData.asMentee?.received?.accepted?.count ?? 0
37 | acceptedCount += homeData.asMentor?.sent?.accepted?.count ?? 0
38 | acceptedCount += homeData.asMentor?.received?.accepted?.count ?? 0
39 |
40 | var rejectedCount = homeData.asMentee?.sent?.rejected?.count ?? 0
41 | rejectedCount += homeData.asMentee?.received?.rejected?.count ?? 0
42 | rejectedCount += homeData.asMentor?.sent?.rejected?.count ?? 0
43 | rejectedCount += homeData.asMentor?.received?.rejected?.count ?? 0
44 |
45 | var cancelledCount = homeData.asMentee?.sent?.cancelled?.count ?? 0
46 | cancelledCount += homeData.asMentee?.received?.cancelled?.count ?? 0
47 | cancelledCount += homeData.asMentor?.sent?.cancelled?.count ?? 0
48 | cancelledCount += homeData.asMentor?.received?.cancelled?.count ?? 0
49 |
50 | var completedCount = homeData.asMentee?.sent?.completed?.count ?? 0
51 | completedCount += homeData.asMentee?.received?.completed?.count ?? 0
52 | completedCount += homeData.asMentor?.sent?.completed?.count ?? 0
53 | completedCount += homeData.asMentor?.received?.completed?.count ?? 0
54 |
55 | return [pendingCount, acceptedCount, rejectedCount, cancelledCount, completedCount]
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/mentorship ios/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleURLTypes
20 |
21 |
22 | CFBundleTypeRole
23 | Editor
24 | CFBundleURLSchemes
25 |
26 |
27 |
28 | CFBundleVersion
29 | 1
30 | LSRequiresIPhoneOS
31 |
32 | UIApplicationSceneManifest
33 |
34 | UIApplicationSupportsMultipleScenes
35 |
36 | UISceneConfigurations
37 |
38 | UIWindowSceneSessionRoleApplication
39 |
40 |
41 | UISceneConfigurationName
42 | Default Configuration
43 | UISceneDelegateClassName
44 | $(PRODUCT_MODULE_NAME).SceneDelegate
45 |
46 |
47 |
48 |
49 | UILaunchStoryboardName
50 | LaunchScreen
51 | UIRequiredDeviceCapabilities
52 |
53 | armv7
54 |
55 | UIStatusBarTintParameters
56 |
57 | UINavigationBar
58 |
59 | Style
60 | UIBarStyleDefault
61 | Translucent
62 |
63 |
64 |
65 | UISupportedInterfaceOrientations
66 |
67 | UIInterfaceOrientationPortrait
68 | UIInterfaceOrientationLandscapeLeft
69 | UIInterfaceOrientationLandscapeRight
70 |
71 | UISupportedInterfaceOrientations~ipad
72 |
73 | UIInterfaceOrientationPortrait
74 | UIInterfaceOrientationPortraitUpsideDown
75 | UIInterfaceOrientationLandscapeLeft
76 | UIInterfaceOrientationLandscapeRight
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/mentorship ios/ViewModels/HomeViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeViewModel.swift
3 | // Created on 21/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 | import Combine
9 |
10 | class HomeViewModel: ObservableObject {
11 |
12 | // MARK: - Variables
13 | @Published var homeResponseData = HomeModel.HomeResponseData(asMentor: nil, asMentee: nil, tasksToDo: nil, tasksDone: nil)
14 | @Published var relationsListData = UIHelper.HomeScreen.RelationsListData()
15 | @Published var userName = ProfileViewModel().profileData.name
16 | @Published var isLoading = false
17 | var firstTimeLoad = true
18 |
19 | // MARK: - Functions
20 | func getSentDetailListData(userType: HomeModel.UserType, index: Int) -> [RequestStructure]? {
21 | if userType == .mentee {
22 | let data1 = homeResponseData.asMentee?.sent
23 | switch index {
24 | case 0: return data1?.pending
25 | case 1: return data1?.accepted
26 | case 2: return data1?.rejected
27 | case 3: return data1?.cancelled
28 | case 4: return data1?.completed
29 | default: return []
30 | }
31 | } else {
32 | let data1 = homeResponseData.asMentor?.sent
33 | switch index {
34 | case 0: return data1?.pending
35 | case 1: return data1?.accepted
36 | case 2: return data1?.rejected
37 | case 3: return data1?.cancelled
38 | case 4: return data1?.completed
39 | default: return []
40 | }
41 | }
42 | }
43 |
44 | func getReceivedDetailListData(userType: HomeModel.UserType, index: Int) -> [RequestStructure]? {
45 | if userType == .mentee {
46 | let data1 = homeResponseData.asMentee?.received
47 | switch index {
48 | case 0: return data1?.pending
49 | case 1: return data1?.accepted
50 | case 2: return data1?.rejected
51 | case 3: return data1?.cancelled
52 | case 4: return data1?.completed
53 | default: return []
54 | }
55 | } else {
56 | let data1 = homeResponseData.asMentor?.received
57 | switch index {
58 | case 0: return data1?.pending
59 | case 1: return data1?.accepted
60 | case 2: return data1?.rejected
61 | case 3: return data1?.cancelled
62 | case 4: return data1?.completed
63 | default: return []
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/mentorship ios/Views/Tasks/TasksSection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TasksToDoSection.swift
3 | // Created on 01/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct TasksSection: View {
10 | let tasks: [TaskStructure]?
11 | //used to enable mark as complete for to do tasks only.
12 | var isToDoSection: Bool = false
13 | var navToTaskComments = false
14 | var markAsCompleteAction: (TaskStructure) -> Void = { _ in }
15 |
16 | var iconName: String {
17 | if isToDoSection {
18 | return ImageNameConstants.SFSymbolConstants.taskToDo
19 | } else {
20 | return ImageNameConstants.SFSymbolConstants.taskDone
21 | }
22 | }
23 |
24 | var taskText: LocalizedStringKey {
25 | if isToDoSection {
26 | return LocalizableStringConstants.tasksToDo
27 | } else {
28 | return LocalizableStringConstants.tasksDone
29 | }
30 | }
31 |
32 | func taskCell(task: TaskStructure) -> some View {
33 | //Main HStack, shows icon and task
34 | HStack {
35 | Button(action: { self.markAsCompleteAction(task) }) {
36 | Image(systemName: self.iconName)
37 | .foregroundColor(DesignConstants.Colors.defaultIndigoColor)
38 | .padding(.trailing, DesignConstants.Padding.insetListCellFrameExpansion)
39 |
40 | }
41 | .buttonStyle(BorderlessButtonStyle())
42 |
43 | Text(task.description ?? "-")
44 | .font(.subheadline)
45 | }
46 | .padding(DesignConstants.Padding.insetListCellFrameExpansion)
47 | //context menu used to show and enable actions (eg. mark as complete)
48 | .contextMenu {
49 | if self.isToDoSection && navToTaskComments {
50 | Button(LocalizableStringConstants.markComplete) { self.markAsCompleteAction(task) }
51 | }
52 | }
53 | }
54 |
55 | var body: some View {
56 | Section(header: Text(taskText).font(.headline)) {
57 | ForEach(tasks ?? []) { task in
58 | if self.navToTaskComments {
59 | //Tapping leads to task comments page
60 | NavigationLink(
61 | destination: TaskComments(taskID: task.id ?? -1, taskName: task.description ?? "")
62 | ) {
63 | self.taskCell(task: task)
64 | }
65 | } else {
66 | self.taskCell(task: task)
67 | }
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/mentorship ios/ViewModels/TaskCommentsViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TaskCommentsViewModel.swift
3 | // Created on 28/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Combine
8 |
9 | class TaskCommentsViewModel: ObservableObject {
10 | @Published var taskCommentsResponse = [TaskCommentsModel.TaskCommentsResponse]()
11 | @Published var newComment = TaskCommentsModel.PostCommentUploadData(comment: "")
12 | // isLoading state, used to show activity indicator while loading.
13 | @Published var isLoading = false
14 | // Initially only the latest comments are shown and earlier comments are hidden.
15 | // Used to handle this state. Also, the button to 'Show Earlier' uses this and is only shown if required.
16 | @Published var showingEarlier = false
17 | // Show message alert if task not added successfully or after report comment operation completes
18 | @Published var showMessageAlert = false
19 | // Show alert to confirm reporting of a task comment action
20 | @Published var showReportViolationAlert = false
21 | // Report of comment in activity, true while service being used
22 | @Published var reportCommentInActivity = false
23 | var taskCommentIDToReport = -1
24 |
25 | // limit of number of latest comments to show
26 | let latestCommentsLimit = 4
27 | // name of other member in relation
28 | var reqName: String = ""
29 | // relation id
30 | var reqID: Int = -1
31 |
32 | // Check if number of comments are more than set limit to show latest comments.
33 | // Used by 'Show Earlier' button to only be visible when required.
34 | var commentsMoreThanLimit: Bool {
35 | return taskCommentsResponse.count > latestCommentsLimit
36 | }
37 |
38 | // Filter comments to be shown. Show all if showEarlier is enabled, else show latest only as per limit.
39 | var commentsToShow: [TaskCommentsModel.TaskCommentsResponse] {
40 | if showingEarlier {
41 | return taskCommentsResponse
42 | } else {
43 | return Array(taskCommentsResponse.suffix(latestCommentsLimit))
44 | }
45 | }
46 |
47 | var sendButtonDisabled: Bool {
48 | return newComment.comment.isEmpty
49 | }
50 |
51 | // Get comment author name.
52 | func getCommentAuthorName(authorID: Int, userID: Int) -> String {
53 | // if author id is same as user id, then author is user.
54 | if authorID == userID {
55 | return LocalizableStringConstants.you
56 | }
57 | // else author is other person in the mentorship relation
58 | else {
59 | return reqName
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/mentorship ios/Views/UtilityViews/SearchNavigation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchNavigation.swift
3 | // Created on 03/08/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct SearchNavigation: UIViewControllerRepresentable {
10 | @Binding var text: String
11 | var search: () -> Void
12 | var cancel: () -> Void
13 | var content: () -> Content
14 |
15 | func makeUIViewController(context: Context) -> UINavigationController {
16 | let navigationController = UINavigationController(rootViewController: context.coordinator.rootViewController)
17 | navigationController.navigationBar.prefersLargeTitles = true
18 |
19 | context.coordinator.searchController.searchBar.delegate = context.coordinator
20 |
21 | return navigationController
22 | }
23 |
24 | func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
25 | context.coordinator.update(content: content())
26 | }
27 |
28 | func makeCoordinator() -> Coordinator {
29 | Coordinator(content: content(), searchText: $text, searchAction: search, cancelAction: cancel)
30 | }
31 |
32 | class Coordinator: NSObject, UISearchBarDelegate {
33 | @Binding var text: String
34 | let rootViewController: UIHostingController
35 | let searchController = UISearchController(searchResultsController: nil)
36 | var search: () -> Void
37 | var cancel: () -> Void
38 |
39 | init(content: Content, searchText: Binding, searchAction: @escaping () -> Void, cancelAction: @escaping () -> Void) {
40 | rootViewController = UIHostingController(rootView: content)
41 | searchController.searchBar.autocapitalizationType = .none
42 | searchController.obscuresBackgroundDuringPresentation = false
43 | rootViewController.navigationItem.searchController = searchController
44 |
45 | _text = searchText
46 | search = searchAction
47 | cancel = cancelAction
48 | }
49 |
50 | func update(content: Content) {
51 | rootViewController.rootView = content
52 | rootViewController.view.setNeedsDisplay()
53 | }
54 |
55 | func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
56 | text = searchText
57 | }
58 |
59 | func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
60 | search()
61 | }
62 |
63 | func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
64 | cancel()
65 | }
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/mentorship ios/Views/Relation/AddTask.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddTask.swift
3 | // Created on 01/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct AddTask: View {
10 | var relationService: RelationService = RelationAPI()
11 | @ObservedObject var relationViewModel: RelationViewModel
12 | @Environment(\.presentationMode) var presentationMode
13 |
14 | // use service to add task
15 | func addTask() {
16 | guard let relationID = self.relationViewModel.currentRelation.id else { return }
17 | self.relationService.addNewTask(newTask: self.relationViewModel.newTask, relationID: relationID) { response in
18 | // if task added successfully, dismiss sheet and refresh tasks
19 | if response.success {
20 | self.relationViewModel.addTask.toggle()
21 | self.relationService.fetchTasks(id: relationID) { (tasks, success) in
22 | self.relationViewModel.handleFetchedTasks(tasks: tasks, success: success)
23 | }
24 | }
25 | // else show error message
26 | else {
27 | response.update(viewModel: self.relationViewModel)
28 | }
29 | }
30 | }
31 |
32 | var body: some View {
33 | NavigationView {
34 | VStack(spacing: DesignConstants.Spacing.bigSpacing) {
35 | //new task description text field
36 | TextField("Task Description", text: $relationViewModel.newTask.description)
37 | .textFieldStyle(RoundFilledTextFieldStyle())
38 |
39 | //add task button
40 | Button("Add") {
41 | self.addTask()
42 | }
43 | .buttonStyle(BigBoldButtonStyle())
44 | .disabled(relationViewModel.addTaskDisabled)
45 | .opacity(relationViewModel.addTaskDisabled ? DesignConstants.Opacity.disabledViewOpacity : 1)
46 |
47 | //error message
48 | Text(self.relationViewModel.responseData.message ?? "")
49 | .modifier(ErrorText())
50 |
51 | //spacer to shift things at top
52 | Spacer()
53 | }
54 | .modifier(AllPadding())
55 | .navigationBarTitle(LocalizableStringConstants.addTask)
56 | .navigationBarItems(trailing: Button(LocalizableStringConstants.cancel) {
57 | self.presentationMode.wrappedValue.dismiss()
58 | })
59 | .onAppear {
60 | // reset the new task description and error message desc to ""
61 | self.relationViewModel.resetDataForAddTaskScreen()
62 | }
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/mentorship ios/Managers/KeychainManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeychainManager.swift
3 | // Created on 06/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 |
9 | struct KeychainManager {
10 | enum KeychainError: Error {
11 | case noPassword
12 | case unexpectedPasswordData
13 | case unhandledError(status: OSStatus)
14 | }
15 |
16 | static func setToken(username: String, tokenString: String) throws {
17 | //try deleting old items if present
18 | do {
19 | try deleteToken()
20 | }
21 | //add new token
22 | let account = username
23 | let token = tokenString.data(using: String.Encoding.utf8)!
24 | let server = baseURL
25 | let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
26 | kSecAttrAccount as String: account,
27 | kSecAttrServer as String: server,
28 | kSecValueData as String: token]
29 | let status = SecItemAdd(query as CFDictionary, nil)
30 | guard status == errSecSuccess else {
31 | fatalError(status.description)
32 | }
33 | }
34 |
35 | static func getToken() throws -> String {
36 | let server = baseURL
37 | let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
38 | kSecAttrServer as String: server,
39 | kSecMatchLimit as String: kSecMatchLimitOne,
40 | kSecReturnAttributes as String: true,
41 | kSecReturnData as String: true]
42 |
43 | var item: CFTypeRef?
44 | let status = SecItemCopyMatching(query as CFDictionary, &item)
45 | guard status != errSecItemNotFound else { throw KeychainError.noPassword }
46 | guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
47 |
48 | guard let existingItem = item as? [String: Any],
49 | let tokenData = existingItem[kSecValueData as String] as? Data,
50 | let token = String(data: tokenData, encoding: String.Encoding.utf8),
51 | let _ = existingItem[kSecAttrAccount as String] as? String
52 | else {
53 | throw KeychainError.unexpectedPasswordData
54 | }
55 | return token
56 | }
57 |
58 | static func deleteToken() throws {
59 | let server = baseURL
60 | let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
61 | kSecAttrServer as String: server]
62 |
63 | let status = SecItemDelete(query as CFDictionary)
64 | guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) }
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/mentorship ios/ViewModels/RelationViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RelationViewModel.swift
3 | // Created on 02/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 | import Combine
9 |
10 | class RelationViewModel: ObservableObject {
11 |
12 | // MARK: - Variables
13 | @Published var currentRelation = RelationModel().currentRelation
14 | @Published var responseData = RelationModel.ResponseData(message: "", success: false)
15 | var tasks = RelationModel().tasks
16 | var firstTimeLoad = true
17 | @Published var newTask = RelationModel.AddTaskData(description: "")
18 | @Published var toDoTasks = RelationModel().tasks
19 | @Published var doneTasks = RelationModel().tasks
20 | @Published var inActivity = false
21 | @Published var addTask = false
22 | @Published var showAlert = false
23 | @Published var showErrorAlert = false
24 | @Published var alertTitle = LocalizableStringConstants.failure
25 | @Published var alertMessage = LocalizedStringKey("")
26 | static var taskTapped = RelationModel().task
27 | private var cancellable: AnyCancellable?
28 |
29 | var addTaskDisabled: Bool {
30 | return newTask.description.isEmpty
31 | }
32 |
33 | var personName: String {
34 | // User profile
35 | let userProfile = ProfileViewModel().getProfile()
36 | //match users name with mentee name.
37 | //if different, return mentee's name. Else, return mentor's name
38 | //Logic: Person with different name is in relation with us.
39 | if currentRelation.mentee?.name != userProfile.name {
40 | return currentRelation.mentee?.name ?? ""
41 | } else {
42 | return currentRelation.mentor?.name ?? ""
43 | }
44 | }
45 |
46 | var personType: LocalizedStringKey {
47 | // User profile
48 | let userProfile = ProfileViewModel().getProfile()
49 | // Person with different name is in relation with us. Hence deduce person type.
50 | if currentRelation.mentee?.name != userProfile.name {
51 | return LocalizableStringConstants.mentee
52 | } else {
53 | return LocalizableStringConstants.mentor
54 | }
55 | }
56 |
57 | // MARK: - Functions
58 |
59 | // resets values in add task screen. Used in onAppear modifier in the view
60 | func resetDataForAddTaskScreen() {
61 | newTask.description = ""
62 | responseData.message = ""
63 | }
64 |
65 | func handleFetchedTasks(tasks: [TaskStructure], success: Bool) {
66 | if success {
67 | doneTasks.removeAll()
68 | toDoTasks.removeAll()
69 | for task in tasks {
70 | task.update(viewModel: self)
71 | }
72 | } else {
73 | showErrorAlert = true
74 | alertMessage = LocalizableStringConstants.operationFail
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/mentorship ios/Service/Networking/SettingsAPI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsAPI.swift
3 | // Created on 24/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 | import Combine
9 |
10 | class SettingsAPI: SettingsService {
11 | private var cancellable: AnyCancellable?
12 | let urlSession: URLSession
13 |
14 | init(urlSession: URLSession = .shared) {
15 | self.urlSession = urlSession
16 | }
17 |
18 | // Delete Account
19 | func deleteAccount(completion: @escaping (SettingsModel.DeleteAccountResponseData) -> Void) {
20 | //get token
21 | guard let token = try? KeychainManager.getToken() else {
22 | return
23 | }
24 |
25 | //api call
26 | cancellable = NetworkManager.callAPI(urlString: URLStringConstants.Users.user, httpMethod: "DELETE", token: token, session: urlSession)
27 | .receive(on: RunLoop.main)
28 | .catch { _ in Just(NetworkResponseModel(message: LocalizableStringConstants.networkErrorString)) }
29 | .sink {
30 | let success = NetworkManager.responseCode == 200
31 | let responseData = SettingsModel.DeleteAccountResponseData(message: $0.message, success: success)
32 | completion(responseData)
33 | }
34 | }
35 |
36 | // Change Password
37 | func changePassword(
38 | changePasswordData: ChangePasswordModel.ChangePasswordUploadData,
39 | confirmPassword: String,
40 | completion: @escaping (ChangePasswordModel.ChangePasswordResponseData) -> Void
41 | ) {
42 | //check password fields
43 | if changePasswordData.newPassword != confirmPassword {
44 | completion(ChangePasswordModel.ChangePasswordResponseData(message: LocalizableStringConstants.passwordsDoNotMatch, success: false))
45 | return
46 | }
47 |
48 | //get auth token
49 | guard let token = try? KeychainManager.getToken() else {
50 | return
51 | }
52 |
53 | //encode upload data
54 | guard let uploadData = try? JSONEncoder().encode(changePasswordData) else {
55 | return
56 | }
57 |
58 | //api call
59 | cancellable = NetworkManager.callAPI(urlString: URLStringConstants.Users.changePassword, httpMethod: "PUT", uploadData: uploadData, token: token, session: urlSession)
60 | .receive(on: RunLoop.main)
61 | .catch { _ in Just(NetworkResponseModel(message: LocalizableStringConstants.networkErrorString)) }
62 | .sink { response in
63 | let success = NetworkManager.responseCode == 201
64 | let responseData = ChangePasswordModel.ChangePasswordResponseData(message: response.message, success: success)
65 | completion(responseData)
66 | }
67 | }
68 |
69 | struct NetworkResponseModel: Decodable {
70 | let message: String?
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/mentorship ios/Constants/URLStringConstants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLConstants.swift
3 | // Created on 05/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | let baseURL: String = "https://mentorship-backend-temp.herokuapp.com/"
8 | ///only for local backend testing
9 | //let baseURL: String = "http://127.0.0.1:5000/"
10 |
11 | struct URLStringConstants {
12 | struct Users {
13 | static let login: String = baseURL + "login"
14 | static let googleAuthCallback = baseURL + "google/auth/callback"
15 | static let appleAuthCallback = baseURL + "apple/auth/callback"
16 | static let signUp: String = baseURL + "register"
17 | static func members(page: Int, perPage: Int, search: String) -> String {
18 | return baseURL + "users?page=\(page)&per_page=\(perPage)&search=\(search)"
19 | }
20 | static let home: String = baseURL + "dashboard"
21 | static let user: String = baseURL + "user"
22 | static let changePassword: String = baseURL + "user/change_password"
23 | }
24 |
25 | struct MentorshipRelation {
26 | static let sendRequest: String = baseURL + "mentorship_relation/send_request"
27 | static let currentRelation: String = baseURL + "mentorship_relations/current"
28 | static func getCurrentTasks(id: Int) -> String {
29 | return baseURL + "mentorship_relation/\(id)/tasks"
30 | }
31 | static func markAsComplete(reqID: Int, taskID: Int) -> String {
32 | return baseURL + "mentorship_relation/\(reqID)/task/\(taskID)/complete"
33 | }
34 | static func addNewTask(reqID: Int) -> String {
35 | return baseURL + "mentorship_relation/\(reqID)/task"
36 | }
37 | static func getTaskComments(reqID: Int, taskID: Int) -> String {
38 | return baseURL + "mentorship_relation/\(reqID)/task/\(taskID)/comments"
39 | }
40 | static func postTaskComment(reqID: Int, taskID: Int) -> String {
41 | return baseURL + "mentorship_relation/\(reqID)/task/\(taskID)/comment"
42 | }
43 | static func reportTaskComment(reqID: Int, taskID: Int, commentID: Int) -> String {
44 | return baseURL + "mentorship_relation/\(reqID)/task/\(taskID)/comment/\(commentID)/report"
45 | }
46 | static func accept(reqID: Int) -> String {
47 | return baseURL + "mentorship_relation/\(reqID)/accept"
48 | }
49 | static func reject(reqID: Int) -> String {
50 | return baseURL + "mentorship_relation/\(reqID)/reject"
51 | }
52 | static func cancel(reqID: Int) -> String {
53 | return baseURL + "mentorship_relation/\(reqID)/cancel"
54 | }
55 | static func delete(reqID: Int) -> String {
56 | return baseURL + "mentorship_relation/\(reqID)"
57 | }
58 | }
59 |
60 | struct WebsiteURLs {
61 | static let privacyPolicy = "https://anitab.org/privacy-policy/"
62 | static let termsOfUse = "https://anitab.org/terms-of-use/"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/mentorship ios.xcodeproj/xcshareddata/xcschemes/mentorship iosTests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
54 |
60 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/mentorship ios.xcodeproj/xcshareddata/xcschemes/mentorship iosUITests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
54 |
60 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/mentorship ios/Views/Members/Members.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Members.swift
3 | // Created on 07/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct Members: View {
10 | var membersService: MembersService = MembersAPI()
11 | @ObservedObject var membersViewModel = MembersViewModel()
12 |
13 | // use service and fetch members
14 | func fetchMembers() {
15 | membersService.fetchMembers(pageToLoad: membersViewModel.currentPage + 1, perPage: membersViewModel.perPage, search: membersViewModel.searchString) { members, listFull in
16 | // to go in update func
17 | self.membersViewModel.currentPage += 1
18 | self.membersViewModel.membersResponseData.append(contentsOf: members)
19 | //if number of members received are less than perPage value
20 | //then it is last page. Used to disable loading in view.
21 | self.membersViewModel.membersListFull = listFull
22 | // if showing original data, backup
23 | if self.membersViewModel.currentlySearching == false {
24 | self.membersViewModel.backup()
25 | }
26 | }
27 | }
28 |
29 | func search() {
30 | membersViewModel.currentlySearching = true
31 | // reset current page
32 | membersViewModel.currentPage = 0
33 | // reset members
34 | membersViewModel.membersResponseData.removeAll()
35 | // reset list full value
36 | // this shows activity indicator which automatically calls fetch members.
37 | membersViewModel.membersListFull = false
38 | }
39 |
40 | func cancelSearch() {
41 | membersViewModel.searchString = ""
42 | membersViewModel.restore()
43 | }
44 |
45 | var body: some View {
46 | SearchNavigation(text: $membersViewModel.searchString, search: search, cancel: cancelSearch) {
47 | // Members List
48 | List {
49 | if self.membersViewModel.membersListFull && self.membersViewModel.membersResponseData.count == 0 {
50 | Text("No member found")
51 | }
52 |
53 | ForEach(self.membersViewModel.membersResponseData) { member in
54 | NavigationLink(destination: MemberDetail(memberData: member)) {
55 | MembersListCell(member: member, membersViewModel: self.membersViewModel)
56 | }
57 | }
58 |
59 | //show acitivty spinner for loading if members list is not full
60 | //activity spinner is the last element of list
61 | //hence its onAppear method is used to load members. Pagination done.
62 | if !self.membersViewModel.membersListFull {
63 | ActivityIndicator(isAnimating: .constant(true))
64 | .onAppear {
65 | self.fetchMembers()
66 | }
67 | }
68 | }
69 | .navigationBarTitle(LocalizableStringConstants.ScreenNames.members)
70 | }
71 | .edgesIgnoringSafeArea(.top)
72 | }
73 | }
74 |
75 | struct Members_Previews: PreviewProvider {
76 | static var previews: some View {
77 | Members()
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/mentorship ios/Views/Tasks/TaskCommentCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TaskCommentCell.swift
3 | // Created on 07/08/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct TaskCommentCell: View {
10 | let comment: TaskCommentsModel.TaskCommentsResponse
11 | let userID: Int
12 | @EnvironmentObject var taskCommentsVM: TaskCommentsViewModel
13 | @State var showActionSheet = false
14 |
15 | func showReportCommentAlert() {
16 | self.taskCommentsVM.taskCommentIDToReport = self.comment.id
17 | self.taskCommentsVM.showReportViolationAlert.toggle()
18 | }
19 |
20 | var body: some View {
21 | VStack(alignment: .leading, spacing: DesignConstants.Form.Spacing.minimalSpacing) {
22 | // Sender name and time
23 | VStack(alignment: .leading, spacing: DesignConstants.Spacing.minimalSpacing) {
24 | // Name and ellipsis icon to show action sheet (if comment by other person)
25 | HStack {
26 | Text(self.taskCommentsVM.getCommentAuthorName(authorID: comment.userID!, userID: userID))
27 | .font(.headline)
28 |
29 | Spacer()
30 |
31 | if comment.userID != self.userID {
32 | Button(action: {
33 | self.showActionSheet.toggle()
34 | }) {
35 | Image(systemName: ImageNameConstants.SFSymbolConstants.ellipsis)
36 | .imageScale(.large)
37 | }
38 | .buttonStyle(BorderlessButtonStyle())
39 | }
40 | }
41 |
42 | // Datetime of comment
43 | Text(DesignConstants.DateFormat.taskTime.string(from: Date(timeIntervalSince1970: comment.creationDate ?? 0)))
44 | .font(.footnote)
45 | .foregroundColor(DesignConstants.Colors.subtitleText)
46 | }
47 | .padding(.vertical, DesignConstants.Padding.textInListCell)
48 |
49 | // Comment
50 | Text(comment.comment ?? "")
51 | .font(.subheadline)
52 | .padding(.bottom, DesignConstants.Padding.textInListCell)
53 | }
54 | .contextMenu {
55 | // If comment is by other person, show report violation button
56 | if comment.userID != self.userID {
57 | Button(action: {
58 | self.showReportCommentAlert()
59 | }) {
60 | HStack {
61 | Text(LocalizableStringConstants.reportComment)
62 | Image(systemName: ImageNameConstants.SFSymbolConstants.reportComment)
63 | }
64 | }
65 | }
66 | }
67 | .actionSheet(isPresented: self.$showActionSheet) {
68 | ActionSheet(
69 | title: Text(LocalizableStringConstants.actionsforComment),
70 | message: Text(comment.comment ?? ""),
71 | buttons: [
72 | .destructive(Text(LocalizableStringConstants.reportComment), action: {
73 | self.showReportCommentAlert()
74 | }),
75 | .cancel()
76 | ])
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/mentorship ios/Managers/SocialSignIn.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GoogleSignInButton.swift
3 | // Created on 09/08/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 | import GoogleSignIn
9 | import AuthenticationServices
10 |
11 | struct SocialSignIn: UIViewRepresentable {
12 |
13 | func makeUIView(context: UIViewRepresentableContext) -> UIView {
14 | return UIView()
15 | }
16 |
17 | func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext) {
18 | }
19 |
20 | // show google sign in flow
21 | func attemptSignInGoogle() {
22 | GIDSignIn.sharedInstance()?.presentingViewController = UIApplication.shared.windows.last?.rootViewController
23 | GIDSignIn.sharedInstance()?.signIn()
24 | }
25 |
26 | // make network request to callback url after recieivng user's data from provider
27 | static func makeNetworkRequest(loginService: LoginService, loginViewModel: LoginViewModel, idToken: String, name: String, email: String, signInType: LoginModel.SocialSignInType) {
28 | loginViewModel.inActivity = true
29 | loginService.socialSignInCallback(
30 | socialSignInData: .init(idToken: idToken, name: name, email: email),
31 | socialSignInType: signInType) { response in
32 | loginViewModel.update(using: response)
33 | loginViewModel.inActivity = false
34 | }
35 | }
36 | }
37 |
38 | // Used in login view model
39 | class AppleSignInCoordinator: NSObject, ASAuthorizationControllerDelegate {
40 | var loginService: LoginService
41 | var loginViewModel: LoginViewModel
42 |
43 | init(loginService: LoginService = LoginAPI(), loginVM: LoginViewModel) {
44 | self.loginViewModel = loginVM
45 | self.loginService = loginService
46 | }
47 |
48 | func handleAuthorizationAppleIDButtonPress() {
49 | let appleIDProvider = ASAuthorizationAppleIDProvider()
50 | let request = appleIDProvider.createRequest()
51 | request.requestedScopes = [.fullName, .email]
52 |
53 | let authorizationController = ASAuthorizationController(authorizationRequests: [request])
54 | authorizationController.delegate = self
55 | authorizationController.performRequests()
56 | }
57 |
58 | // Delegate methods
59 |
60 | func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
61 |
62 | switch authorization.credential {
63 | case let appleIDCredential as ASAuthorizationAppleIDCredential:
64 |
65 | // Get user details
66 | let userIdentifier = appleIDCredential.user
67 | let fullName = appleIDCredential.fullName
68 | let email = appleIDCredential.email ?? ""
69 | let name = (fullName?.givenName ?? "") + (" ") + (fullName?.familyName ?? "")
70 |
71 | // Make network request to backend
72 | SocialSignIn.makeNetworkRequest(loginService: loginService, loginViewModel: loginViewModel, idToken: userIdentifier, name: name, email: email, signInType: .apple)
73 |
74 |
75 | default:
76 | break
77 | }
78 | }
79 |
80 | func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
81 | print(error.localizedDescription)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/.github/reporting_guidelines.md:
--------------------------------------------------------------------------------
1 | # Reporting Guidelines
2 |
3 | If you believe someone is violating the code of conduct we ask that you report it to the community admins by emailing opensource@anitab.org.
4 |
5 | **All reports will be kept confidential**. In some cases we may determine that a public statement will need to be made. If that's the case, the identities of all victims and reporters will remain confidential unless those individuals instruct us otherwise.
6 |
7 | If you believe anyone is in physical danger, please notify appropriate emergency services first. If you are unsure what service or agency is appropriate to contact, include this in your report and we will attempt to notify them.
8 |
9 | In your report please include:
10 |
11 | * Your contact info for follow-up contact.
12 | * Names (legal, nicknames, or pseudonyms) of any individuals involved.
13 | * If there were other witnesses besides you, please try to include them as well.
14 | * When and where the incident occurred. Please be as specific as possible.
15 | * Your account of what occurred.
16 | * If there is a publicly available record (e.g. a mailing list archive or a public IRC logger) please include a link.
17 | * Any extra context you believe existed for the incident.
18 | * If you believe this incident is ongoing.
19 | * Any other information you believe we should have.
20 |
21 |
22 |
23 | ## What happens after you file a report?
24 |
25 | You will receive an email from the AnitaB.org Open Source's Code of Conduct response team acknowledging receipt as soon as possible, but within 48 hours.
26 |
27 | The working group will immediately meet to review the incident and determine:
28 |
29 | * What happened.
30 | * Whether this event constitutes a code of conduct violation.
31 | * What kind of response is appropriate.
32 |
33 | If this is determined to be an ongoing incident or a threat to physical safety, the team's immediate priority will be to protect everyone involved. This means we may delay an "official" response until we believe that the situation has ended and that everyone is physically safe.
34 |
35 | Once the team has a complete account of the events they will make a decision as to how to respond. Responses may include:
36 |
37 | Nothing (if we determine no code of conduct violation occurred).
38 | * A private reprimand from the Code of Conduct response team to the individual(s) involved.
39 | * A public reprimand.
40 | * An imposed vacation (i.e. asking someone to "take a week off" from a mailing list or IRC).
41 | * A permanent or temporary ban from some or all of AnitaB.org spaces (events, meetings, mailing lists, IRC, etc.)
42 | * A request to engage in mediation and/or an accountability plan.
43 | We'll respond within one week to the person who filed the report with either a resolution or an explanation of why the situation is not yet resolved.
44 |
45 | Once we've determined our final action, we'll contact the original reporter to let them know what action (if any) we'll be taking. We'll take into account feedback from the reporter on the appropriateness of our response, but our response will be determined by what will be best for community safety.
46 |
47 | Finally, the response team will make a report on the situation to the AnitaB.org's Open Source Board. The board may choose to issue a public report of the incident or take additional actions.
48 |
49 |
50 |
51 | ## Appealing the response
52 |
53 | Only permanent resolutions (such as bans) may be appealed. To appeal a decision of the working group, contact the community admins at opensource@anitab.org with your appeal and we will review the case.
54 |
--------------------------------------------------------------------------------
/mentorship ios/Constants/DesignConstants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DesignConstants.swift
3 | // Created on 04/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct DesignConstants {
10 |
11 | struct Spacing {
12 | static let bigSpacing: CGFloat = 48
13 | static let smallSpacing: CGFloat = 16
14 | static let minimalSpacing: CGFloat = 2
15 | }
16 |
17 | struct Screen {
18 | struct Padding {
19 | // default SwiftUI padding value = 16
20 | static let topPadding: CGFloat = 16
21 | static let bottomPadding: CGFloat = 16
22 | static let leadingPadding: CGFloat = 16
23 | static let trailingPadding: CGFloat = 16
24 | }
25 | }
26 |
27 | struct Form {
28 | struct Spacing {
29 | static let bigSpacing: CGFloat = 46
30 | static let smallSpacing: CGFloat = 16
31 | static let minimalSpacing: CGFloat = 6
32 | }
33 |
34 | struct Padding {
35 | static let topPadding: CGFloat = 16 * 2
36 | }
37 | }
38 |
39 | struct Padding {
40 | //used to expand frame, eg. of textfield
41 | static let textFieldFrameExpansion: CGFloat = 10
42 | static let listCellFrameExpansion: CGFloat = 10
43 | static let insetListCellFrameExpansion: CGFloat = 6
44 | static let textInListCell: CGFloat = 4
45 | }
46 |
47 | struct Width {
48 | static let listCellTitle: CGFloat = 120
49 | }
50 |
51 | struct Height {
52 | static let textViewHeight: CGFloat = 100
53 | static let socialSignInButton: CGFloat = 47
54 | }
55 |
56 | struct CornerRadius {
57 | static let preferredCornerRadius: CGFloat = 6
58 | }
59 |
60 | struct Opacity {
61 | static let disabledViewOpacity: Double = 0.75
62 | static let tapHighlightingOpacity: Double = 0.75
63 | }
64 |
65 | struct Blur {
66 | static let backgroundBlur: CGFloat = 8
67 | }
68 |
69 | struct Colors {
70 | static let defaultIndigoColor = Color(.systemIndigo)
71 | static let primaryBackground = Color(.systemBackground)
72 | static let secondaryBackground = Color(.secondarySystemBackground)
73 | static let formBackgroundColor = Color(.systemGroupedBackground)
74 | static let subtitleText = Color.secondary
75 | static let userError = Color.red
76 | static let pending = Color.blue
77 | static let accepted = Color.green
78 | static let rejected = Color.pink
79 | static let cancelled = Color.gray
80 |
81 | static let indigoUIColor = UIColor.systemIndigo
82 | static let secondaryUIBackground = UIColor.secondarySystemGroupedBackground
83 | }
84 |
85 | struct Fonts {
86 | static let userError = Font.subheadline
87 |
88 | struct Size {
89 | static let insetListIcon: CGFloat = 20
90 | static let navBarIcon: CGFloat = 30
91 | }
92 | }
93 |
94 | struct DateFormat {
95 | static var mediumDate: DateFormatter {
96 | let formatter = DateFormatter()
97 | formatter.dateStyle = .medium
98 | formatter.timeStyle = .none
99 | return formatter
100 | }
101 |
102 | static var taskTime: DateFormatter {
103 | let formatter = DateFormatter()
104 | formatter.doesRelativeDateFormatting = true
105 | formatter.dateStyle = .short
106 | formatter.timeStyle = .short
107 | return formatter
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/mentorship ios/Service/Networking/LoginAPI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoginAPI.swift
3 | // Created on 23/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 | import Combine
9 |
10 | class LoginAPI: LoginService {
11 | private var cancellable: AnyCancellable?
12 | let urlSession: URLSession
13 |
14 | init(urlSession: URLSession = .shared) {
15 | self.urlSession = urlSession
16 | }
17 |
18 | private func makeSignInNetworkRequest(urlString: String, uploadData: Data, keychainUsername: String, completion: @escaping (LoginModel.LoginResponseData) -> Void) {
19 | // make network request
20 | cancellable = NetworkManager.callAPI(urlString: urlString, httpMethod: "POST", uploadData: uploadData, session: urlSession)
21 | .receive(on: RunLoop.main)
22 | .catch { _ in Just(LoginNetworkModel(message: LocalizableStringConstants.networkErrorString)) }
23 | .sink { response in
24 | var loginResponseData = LoginModel.LoginResponseData(message: response.message)
25 | // if login successful, store access token in keychain
26 | if var token = response.accessToken {
27 | token = "Bearer " + token
28 | do {
29 | try KeychainManager.setToken(username: keychainUsername, tokenString: token)
30 | UserDefaults.standard.set(true, forKey: UserDefaultsConstants.isLoggedIn)
31 | } catch {
32 | loginResponseData.message = "Failed to save access token"
33 | }
34 | }
35 | // completion handler
36 | completion(loginResponseData)
37 | }
38 | }
39 |
40 | func login(
41 | loginData: LoginModel.LoginUploadData,
42 | completion: @escaping (LoginModel.LoginResponseData) -> Void
43 | ) {
44 | // encode upload data
45 | guard let uploadData = try? JSONEncoder().encode(loginData) else {
46 | return
47 | }
48 |
49 | // make network request
50 | makeSignInNetworkRequest(
51 | urlString: URLStringConstants.Users.login,
52 | uploadData: uploadData,
53 | keychainUsername: loginData.username) { response in
54 | completion(response)
55 | }
56 | }
57 |
58 | func socialSignInCallback(
59 | socialSignInData: LoginModel.SocialSignInData,
60 | socialSignInType: LoginModel.SocialSignInType,
61 | completion: @escaping (LoginModel.LoginResponseData) -> Void
62 | ) {
63 | // encode upload data
64 | guard let uploadData = try? JSONEncoder().encode(socialSignInData) else {
65 | return
66 | }
67 |
68 | // set URL depending upon auth provider used
69 | var urlString = ""
70 | switch socialSignInType {
71 | case .apple:
72 | urlString = URLStringConstants.Users.appleAuthCallback
73 | case .google:
74 | urlString = URLStringConstants.Users.googleAuthCallback
75 | }
76 |
77 | // make network request
78 | makeSignInNetworkRequest(
79 | urlString: urlString,
80 | uploadData: uploadData,
81 | keychainUsername: socialSignInData.email) { response in
82 | completion(response)
83 | }
84 | }
85 |
86 | struct LoginNetworkModel: Decodable {
87 | let message: String?
88 | var accessToken: String?
89 |
90 | enum CodingKeys: String, CodingKey {
91 | case message
92 | case accessToken = "access_token"
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/mentorship ios/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Created on 30/05/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import UIKit
8 | import SwiftUI
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
15 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
16 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
17 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
18 |
19 | // Get the managed object context from the shared persistent container.
20 | guard let context = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer.viewContext else { return }
21 |
22 | // Create the SwiftUI view and set the context as the value for the managedObjectContext environment keyPath.
23 | // Add `@Environment(\.managedObjectContext)` in the views that will need the context.
24 | let contentView = ContentView().environment(\.managedObjectContext, context)
25 |
26 | // Use a UIHostingController as window root view controller.
27 | if let windowScene = scene as? UIWindowScene {
28 | let loginViewModel = (UIApplication.shared.delegate as? AppDelegate)!.loginViewModel
29 | let window = UIWindow(windowScene: windowScene)
30 | window.rootViewController = UIHostingController(rootView: contentView.environmentObject(loginViewModel))
31 | self.window = window
32 | window.makeKeyAndVisible()
33 |
34 | self.window?.tintColor = DesignConstants.Colors.indigoUIColor
35 | }
36 | }
37 |
38 | func sceneDidDisconnect(_ scene: UIScene) {
39 | // Called as the scene is being released by the system.
40 | // This occurs shortly after the scene enters the background, or when its session is discarded.
41 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
42 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
43 | }
44 |
45 | func sceneDidBecomeActive(_ scene: UIScene) {
46 | // Called when the scene has moved from an inactive state to an active state.
47 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
48 | }
49 |
50 | func sceneWillResignActive(_ scene: UIScene) {
51 | // Called when the scene will move from an active state to an inactive state.
52 | // This may occur due to temporary interruptions (ex. an incoming phone call).
53 | }
54 |
55 | func sceneWillEnterForeground(_ scene: UIScene) {
56 | // Called as the scene transitions from the background to the foreground.
57 | // Use this method to undo the changes made on entering the background.
58 | }
59 |
60 | func sceneDidEnterBackground(_ scene: UIScene) {
61 | // Called as the scene transitions from the foreground to the background.
62 | // Use this method to save data, release shared resources, and store enough scene-specific state information
63 | // to restore the scene back to its current state.
64 |
65 | // Save changes in the application's managed object context when the application transitions to the background.
66 | (UIApplication.shared.delegate as? AppDelegate)?.saveContext()
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/mentorship ios/Views/Settings/IndividualSetting/ChangePassword.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChangePassword.swift
3 | // Created on 26/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct ChangePassword: View {
10 | var settingsService: SettingsService = SettingsAPI()
11 | @ObservedObject var changePasswordViewModel = ChangePasswordViewModel()
12 | @Environment(\.presentationMode) var presentationMode
13 |
14 | // use service to change password
15 | func changePassword() {
16 | self.changePasswordViewModel.inActivity = true
17 | // make request
18 | self.settingsService.changePassword(
19 | changePasswordData: self.changePasswordViewModel.changePasswordData,
20 | confirmPassword: self.changePasswordViewModel.confirmPassword) { response in
21 | self.changePasswordViewModel.changePasswordResponseData = response
22 | self.changePasswordViewModel.inActivity = false
23 | }
24 | }
25 |
26 | var body: some View {
27 | VStack(spacing: DesignConstants.Form.Spacing.bigSpacing) {
28 | //input fields
29 | VStack(spacing: DesignConstants.Form.Spacing.smallSpacing) {
30 | SecureField("Current Password", text: $changePasswordViewModel.changePasswordData.currentPassword)
31 | .textFieldStyle(RoundFilledTextFieldStyle())
32 | SecureField("New Password", text: $changePasswordViewModel.changePasswordData.newPassword)
33 | .textFieldStyle(RoundFilledTextFieldStyle())
34 | SecureField("Confirm Password", text: $changePasswordViewModel.confirmPassword)
35 | .textFieldStyle(RoundFilledTextFieldStyle())
36 | }
37 |
38 | //change password button
39 | Button(LocalizableStringConstants.confirm) {
40 | self.changePassword()
41 | }
42 | .buttonStyle(BigBoldButtonStyle(disabled: changePasswordViewModel.changePasswordDisabled))
43 | .disabled(changePasswordViewModel.changePasswordDisabled)
44 |
45 | //activity indicator or show user message text
46 | if self.changePasswordViewModel.inActivity {
47 | ActivityIndicator(isAnimating: $changePasswordViewModel.inActivity, style: .medium)
48 | } else if !self.changePasswordViewModel.changePasswordResponseData.success {
49 | Text(self.changePasswordViewModel.changePasswordResponseData.message ?? "An error occurred")
50 | .modifier(ErrorText())
51 | }
52 |
53 | //spacer to push things to top
54 | Spacer()
55 | }
56 | .modifier(AllPadding())
57 | .navigationBarTitle("Change Password")
58 | .alert(isPresented: $changePasswordViewModel.changePasswordResponseData.success) {
59 | Alert(
60 | title: Text(LocalizableStringConstants.success),
61 | message: Text(self.changePasswordViewModel.changePasswordResponseData.message ?? "Password updated successfully"),
62 | dismissButton: .default(Text(LocalizableStringConstants.okay)) {
63 | //pop navigation view after okay button pressed
64 | self.presentationMode.wrappedValue.dismiss()
65 | self.changePasswordViewModel.changePasswordResponseData.success = true
66 | })
67 | }
68 | .onDisappear {
69 | self.changePasswordViewModel.resetData()
70 | }
71 | }
72 | }
73 |
74 | struct ChangePassword_Previews: PreviewProvider {
75 | static var previews: some View {
76 | ChangePassword()
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/mentorship ios/ViewModels/ProfileViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileViewModel.swift
3 | // Created on 21/06/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 | import Combine
9 |
10 | class ProfileViewModel: ObservableObject {
11 |
12 | // MARK: - Variables
13 | let profileData = ProfileModel.ProfileData(
14 | id: 0,
15 | name: "",
16 | username: "",
17 | email: "",
18 | bio: "",
19 | location: "",
20 | occupation: "",
21 | organization: "",
22 | slackUsername: "",
23 | skills: "",
24 | interests: "",
25 | needMentoring: false,
26 | availableToMentor: false
27 | )
28 | @Published var updateProfileResponseData = ProfileModel.UpdateProfileResponseData(success: false, message: "")
29 | @Published var inActivity = false
30 | @Published var showAlert = false
31 | var alertTitle = LocalizedStringKey("")
32 |
33 | // MARK: - Functions
34 |
35 | //saves profile in user defaults
36 | func saveProfile(profile: ProfileModel.ProfileData) {
37 | guard let profileData = try? JSONEncoder().encode(profile) else {
38 | return
39 | }
40 | UserDefaults.standard.set(profileData, forKey: UserDefaultsConstants.profile)
41 | }
42 |
43 | //gets profile object from user defaults
44 | func getProfile() -> ProfileModel.ProfileData {
45 | // Get profile as Data from userdefaults.
46 | guard let profileData = UserDefaults.standard.data(forKey: UserDefaultsConstants.profile) else {
47 | return self.profileData
48 | }
49 | // Use profile received as data to convert to desired by decoding
50 | guard let profile = try? JSONDecoder().decode(ProfileModel.ProfileData.self, from: profileData) else {
51 | return self.profileData
52 | }
53 | // return profile
54 | return profile
55 | }
56 |
57 | //returns profile data with some processing to make it suitable for use in profile editor
58 | func getEditProfileData() -> ProfileModel.ProfileData {
59 | var editProfileData = getProfile()
60 |
61 | //Replace nil values with empty values.
62 | //Done to enable force-unwrap of binding, to be used in edit text field in profile editor.
63 | //Optional bindings are not allowed.
64 | if editProfileData.name == nil { editProfileData.name = "" }
65 | if editProfileData.bio == nil { editProfileData.bio = "" }
66 | if editProfileData.location == nil { editProfileData.location = "" }
67 | if editProfileData.occupation == nil { editProfileData.occupation = "" }
68 | if editProfileData.organization == nil { editProfileData.organization = "" }
69 | if editProfileData.slackUsername == nil { editProfileData.slackUsername = "" }
70 | if editProfileData.skills == nil { editProfileData.skills = "" }
71 | if editProfileData.interests == nil { editProfileData.interests = "" }
72 | if editProfileData.needMentoring == nil { editProfileData.needMentoring = false }
73 | if editProfileData.availableToMentor == nil { editProfileData.availableToMentor = false }
74 |
75 | //Set username to nil.
76 | //Reason: username can't be updated.
77 | //Sending nil username to server keeps it unchanged.
78 | editProfileData.username = nil
79 |
80 | return editProfileData
81 | }
82 |
83 | //func to save updated profile in user defaults
84 | func saveUpdatedProfile(updatedProfileData: ProfileModel.ProfileData) {
85 | var newProfileData = updatedProfileData
86 | newProfileData.username = getProfile().username
87 | self.saveProfile(profile: newProfileData)
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/mentorship ios/Service/ServiceProtocols.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RequestActionService.swift
3 | // Created on 22/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | // MARK: - Login Service.
8 | // To login into main app.
9 | protocol LoginService {
10 | func login(
11 | loginData: LoginModel.LoginUploadData,
12 | completion: @escaping (LoginModel.LoginResponseData) -> Void
13 | )
14 |
15 | func socialSignInCallback(
16 | socialSignInData: LoginModel.SocialSignInData,
17 | socialSignInType: LoginModel.SocialSignInType,
18 | completion: @escaping (LoginModel.LoginResponseData) -> Void
19 | )
20 | }
21 |
22 | // MARK: - SignUp Service
23 | // To sign up as a new user.
24 | protocol SignUpService {
25 | func signUp(
26 | availabilityPickerSelection: Int,
27 | signUpData: SignUpModel.SignUpUploadData,
28 | confirmPassword: String,
29 | completion: @escaping (SignUpModel.SignUpResponseData) -> Void
30 | )
31 | }
32 |
33 | // MARK: - Home Service
34 | // To fetch dashboard data and populate home screen
35 | protocol HomeService {
36 | func fetchDashboard(completion: @escaping (HomeModel.HomeResponseData) -> Void)
37 | }
38 |
39 | // MARK: - Request Action Service
40 | // To accept, reject, delete, or cancel a request
41 | protocol RequestActionService {
42 | func actOnPendingRequest(
43 | action: ActionType,
44 | reqID: Int,
45 | completion: @escaping (RequestActionResponse, Bool) -> Void
46 | )
47 | }
48 |
49 | // MARK: - Profile Service
50 | // To fetch and update user profile
51 | protocol ProfileService {
52 | func getProfile(completion: @escaping (ProfileModel.ProfileData) -> Void)
53 |
54 | func updateProfile(
55 | updateProfileData: ProfileModel.ProfileData,
56 | completion: @escaping (ProfileModel.UpdateProfileResponseData) -> Void
57 | )
58 | }
59 |
60 | // MARK: - Relation Service
61 | protocol RelationService {
62 | func fetchCurrentRelation(completion: @escaping (RequestStructure) -> Void)
63 |
64 | func fetchTasks(id: Int, completion: @escaping ([TaskStructure], Bool) -> Void)
65 |
66 | func addNewTask(newTask: RelationModel.AddTaskData, relationID: Int, completion: @escaping (RelationModel.ResponseData) -> Void)
67 |
68 | func markAsComplete(taskID: Int, relationID: Int, completion: @escaping (RelationModel.ResponseData) -> Void)
69 | }
70 |
71 | // MARK: - Task Comments Service
72 | protocol TaskCommentsService {
73 | func fetchTaskComments(
74 | reqID: Int,
75 | taskID: Int,
76 | completion: @escaping ([TaskCommentsModel.TaskCommentsResponse]) -> Void
77 | )
78 |
79 | func postTaskComment(
80 | reqID: Int,
81 | taskID: Int,
82 | commentData: TaskCommentsModel.PostCommentUploadData,
83 | completion: @escaping (TaskCommentsModel.MessageResponse) -> Void
84 | )
85 |
86 | func reportComment(
87 | reqID: Int,
88 | taskID: Int,
89 | commentID: Int,
90 | completion: @escaping (TaskCommentsModel.MessageResponse) -> Void
91 | )
92 | }
93 |
94 | // MARK: - Members Service
95 | protocol MembersService {
96 | func fetchMembers(pageToLoad: Int, perPage: Int, search: String, completion: @escaping ([MembersModel.MembersResponseData], Bool) -> Void)
97 |
98 | func sendRequest(menteeID: Int, mentorID: Int, endDate: Double, notes: String, completion: @escaping (MembersModel.SendRequestResponseData) -> Void)
99 | }
100 |
101 | // MARK: - Settings Service
102 | protocol SettingsService {
103 | func deleteAccount(completion: @escaping (SettingsModel.DeleteAccountResponseData) -> Void)
104 |
105 | func changePassword(
106 | changePasswordData: ChangePasswordModel.ChangePasswordUploadData,
107 | confirmPassword: String,
108 | completion: @escaping (ChangePasswordModel.ChangePasswordResponseData) -> Void
109 | )
110 | }
111 |
--------------------------------------------------------------------------------
/mentorship iosTests/HomeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeTests.swift
3 | // Created on 26/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import XCTest
8 | @testable import mentorship_ios
9 |
10 | class HomeTests: XCTestCase {
11 | // custom urlsession for mock network calls
12 | var urlSession: URLSession!
13 |
14 | override func setUpWithError() throws {
15 | // Set url session for mock networking
16 | let configuration = URLSessionConfiguration.ephemeral
17 | configuration.protocolClasses = [MockURLProtocol.self]
18 | urlSession = URLSession(configuration: configuration)
19 |
20 | // set temp keychain token
21 | // api calls require a token, otherwise tests fail
22 | try KeychainManager.setToken(username: "", tokenString: "")
23 | }
24 |
25 | override func tearDownWithError() throws {
26 | urlSession = nil
27 | try KeychainManager.deleteToken()
28 | super.tearDown()
29 | }
30 |
31 | // MARK: - Serivce Tests
32 | func testRequestAction() throws {
33 | // Set mock json for server response
34 | let mockJSON = RequestActionResponse(message: "response message")
35 | // Create mock data from mock json
36 | let mockData = try JSONEncoder().encode(mockJSON)
37 |
38 | // Use mock protocol, and return mock data and url response from handler
39 | MockURLProtocol.requestHandler = { request in
40 | return (HTTPURLResponse(), mockData)
41 | }
42 |
43 | // Expectation. Used for testing async code.
44 | let expectation = XCTestExpectation(description: "response")
45 |
46 | // Declare service and test response
47 | let requestActionService: RequestActionService = RequestActionAPI(urlSession: urlSession)
48 | requestActionService.actOnPendingRequest(action: .accept, reqID: 0) { response, _ in
49 | // Test if correct response is returned.
50 | XCTAssertEqual(response.message, mockJSON.message)
51 | expectation.fulfill()
52 | }
53 | wait(for: [expectation], timeout: 1)
54 | }
55 |
56 | func testHomeService() throws {
57 | // Home Service
58 | let homeService: HomeService = HomeAPI(urlSession: urlSession)
59 |
60 | // Set mock json and data
61 | let mockJSON = homeResponseJSONString
62 | let mockData = mockJSON.data(using: .utf8)!
63 |
64 | // Return data in mock request handler
65 | MockURLProtocol.requestHandler = { request in
66 | return (HTTPURLResponse(), mockData)
67 | }
68 |
69 | // Set expectation. Used to test async code.
70 | let expectation = XCTestExpectation(description: "response")
71 |
72 | // Make fetch dashboard request and test response data.
73 | homeService.fetchDashboard { resp in
74 | XCTAssertEqual(resp.tasksToDo?.count, 1)
75 | XCTAssertEqual(resp.tasksDone?.count, 2)
76 | expectation.fulfill()
77 | }
78 | wait(for: [expectation], timeout: 1)
79 | }
80 |
81 | func testUserFirstName() {
82 |
83 | //Test computed first name
84 | let testHome = Home()
85 |
86 | // Set User name for general first name isolation
87 | testHome.homeViewModel.userName = "Jane Alice Smith"
88 | XCTAssertEqual(testHome.userFirstName, "Jane")
89 |
90 | // Set User name with leading spaces
91 | testHome.homeViewModel.userName = " Jane Alice Smith"
92 | XCTAssertEqual(testHome.userFirstName, "Jane")
93 |
94 | // Set User name for proper name capitalization
95 | testHome.homeViewModel.userName = "jaNe Alice Smith"
96 | XCTAssertEqual(testHome.userFirstName, "Jane")
97 |
98 | // Set User name as optional nil
99 | testHome.homeViewModel.userName = nil
100 | XCTAssertEqual(testHome.userFirstName, "")
101 |
102 | }
103 |
104 | }
105 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Mentorship System (iOS)
2 |
3 | [Mentorship System](https://github.com/anitab-org/mentorship-backend) is an application that allows women in tech to mentor each other, on career development topics, through 1:1 relations for a certain period.
4 |
5 | This is the iOS client for the Mentorship System.
6 |
7 | 
8 | *Visit [here](https://github.com/yugantarjain/mentorship-ios/blob/screenshots/Docs/Screenshots.md) to see all the screenshots*
9 |
10 | ## Setting up the project
11 |
12 | 1. Make sure you have Xcode IDE downloaded on your machine for development.
13 | 2. Fork the project. Go to [mentorship-ios](https://github.com/anitab-org/mentorship-ios) and click on Fork in the top right corner to fork the repository to your Github account.
14 | 3. Clone the project. Open the forked mentorship-ios repository from your GitHub account and click on the "Clone or Download" button. You should be able to see the option "Open in Xcode"; this is the recommended option and should be used to get a local copy of the project on your machine.
15 | 4. Open your terminal and go to the project folder on your machine. Run the command `pod install` (note: you may first need to install cocoapods using `sudo gem install cocoapods`). A new .xcworkspace file shall be created.
16 | 4. You're all set now! Use the .xcworkspace file for development.
17 |
18 | ## Setting up social sign-in (Optional)
19 | The Mentorship iOS app currently supports two different social sign in providers - Apple and Google.
20 | The set-up for both of these is a little different and are explained below-
21 | #### Set-up Sign in with Apple
22 | 1. You will need an Apple Developer Account and the 'Account Holder' or 'Admin' rights for that account.
23 | 2. Add capability in Xcode project as explained [here](https://help.apple.com/developer-account/?lang=en#/devde676e696).
24 | 3. Register outbound email domain (mentorshiptest@mail.com) on your Apple Developer account. [Instructions](https://help.apple.com/developer-account/?lang=en#/devf822fb8fc)
25 | #### Set-up Sign in with Google
26 | 1. Generate OAuth client id for project from [here](https://developers.google.com/identity/sign-in/ios/start?ver=swift). You shall get a Configuration.plist file, save that for convenience.
27 | 2. Add reversed client id (from Configuration.plist file) as an URL type in Xcode project.
28 | 3. Add the client id in Config.plist file in the project for the key 'googleAuthClientId'.
29 | 4. To test Sign in with Google, you'll have to set-up the backend locally and use this same client-id as an environment variable. Please see the [instructions](https://github.com/anitab-org/mentorship-backend) for setting up the backend locally.
30 |
31 | ## Contributing
32 |
33 | For contributing, you first need to configure the remotes and then submit pull requests with your changes. Both of these steps are explained in detail in the links below and we recommend all contributors to go through them-
34 |
35 | 1. [Configuring Remotes](https://github.com/anitab-org/mentorship-ios/blob/develop/Docs/Configuring%20Remotes.md)
36 | 2. [Contributing and developing a feature](https://github.com/anitab-org/mentorship-ios/blob/develop/Docs/Contributing%20and%20Developing.md)
37 |
38 | Please read our [Contributing Guidelines](https://github.com/anitab-org/mentorship-ios/blob/develop/.github/contributing_guidelines.md), [Code of Conduct](https://github.com/anitab-org/mentorship-ios/blob/develop/.github/code_of_conduct.md), and [Reporting Guidelines](https://github.com/anitab-org/mentorship-ios/blob/develop/.github/reporting_guidelines.md).
39 |
40 | ## Contact
41 |
42 | You can reach our community at [AnitaB.org Open Source Zulip](https://anitab-org.zulipchat.com/).
43 |
44 | We use [#mentorship-system](https://anitab-org.zulipchat.com/#narrow/stream/222534-mentorship-system) stream on Zulip to discuss this project and interact with the community. If you're interested in contributing to this project, join us there!
45 |
46 | ## License
47 |
48 | Mentorship System is licensed under the GNU General Public License v3.0. Learn more about it in the [LICENSE](LICENSE) file.
49 |
--------------------------------------------------------------------------------
/mentorship ios/Service/Networking/RelationAPI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RelationAPI.swift
3 | // Created on 23/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 | import Combine
9 |
10 | class RelationAPI: RelationService {
11 | private var cancellable: AnyCancellable?
12 | private var tasksCancellable: AnyCancellable?
13 | let urlSession: URLSession
14 |
15 | init(urlSession: URLSession = .shared) {
16 | self.urlSession = urlSession
17 | }
18 |
19 | func fetchCurrentRelation(completion: @escaping (RequestStructure) -> Void) {
20 | //get auth token
21 | guard let token = try? KeychainManager.getToken() else {
22 | return
23 | }
24 |
25 | //api call
26 | cancellable = NetworkManager.callAPI(urlString: URLStringConstants.MentorshipRelation.currentRelation, token: token, session: urlSession)
27 | .receive(on: RunLoop.main)
28 | .catch { _ in Just(RelationModel().currentRelation) }
29 | .sink {
30 | completion($0)
31 | }
32 | }
33 |
34 | func fetchTasks(id: Int, completion: @escaping ([TaskStructure], Bool) -> Void) {
35 | //get auth token
36 | guard let token = try? KeychainManager.getToken() else {
37 | return
38 | }
39 |
40 | // make api call
41 | tasksCancellable = NetworkManager.callAPI(urlString: URLStringConstants.MentorshipRelation.getCurrentTasks(id: id), token: token, session: urlSession)
42 | .receive(on: RunLoop.main)
43 | .catch { _ in Just(RelationModel().tasks) }
44 | .sink {
45 | let success = NetworkManager.responseCode == 200
46 | completion($0, success)
47 | }
48 | }
49 |
50 | //create newtask api call
51 | func addNewTask(newTask: RelationModel.AddTaskData, relationID: Int, completion: @escaping (RelationModel.ResponseData) -> Void) {
52 | //get auth token
53 | guard let token = try? KeychainManager.getToken() else {
54 | return
55 | }
56 |
57 | //prepare upload data
58 | guard let uploadData = try? JSONEncoder().encode(newTask) else {
59 | return
60 | }
61 |
62 | //api call
63 | cancellable = NetworkManager.callAPI(
64 | urlString: URLStringConstants.MentorshipRelation.addNewTask(reqID: relationID),
65 | httpMethod: "POST",
66 | uploadData: uploadData,
67 | token: token,
68 | session: urlSession)
69 | .receive(on: RunLoop.main)
70 | .catch { _ in Just(NetworkResponse(message: LocalizableStringConstants.networkErrorString)) }
71 | .sink {
72 | let success = NetworkManager.responseCode == 201
73 | let response = RelationModel.ResponseData(message: $0.message, success: success)
74 | completion(response)
75 | }
76 | }
77 |
78 | //mark task as complete api call
79 | func markAsComplete(taskID: Int, relationID: Int, completion: @escaping (RelationModel.ResponseData) -> Void) {
80 | //get auth token
81 | guard let token = try? KeychainManager.getToken() else {
82 | return
83 | }
84 |
85 | //api call
86 | cancellable = NetworkManager.callAPI(
87 | urlString: URLStringConstants.MentorshipRelation.markAsComplete(reqID: relationID, taskID: taskID),
88 | httpMethod: "PUT",
89 | token: token,
90 | session: urlSession)
91 | .receive(on: RunLoop.main)
92 | .catch { _ in Just(NetworkResponse(message: LocalizableStringConstants.networkErrorString)) }
93 | .sink {
94 | let success = NetworkManager.responseCode == 200
95 | let response = RelationModel.ResponseData(message: $0.message, success: success)
96 | completion(response)
97 | }
98 | }
99 |
100 | struct NetworkResponse: Decodable {
101 | let message: String?
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/mentorship ios.xcodeproj/xcshareddata/xcschemes/mentorship ios.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
43 |
49 |
50 |
51 |
52 |
53 |
63 |
65 |
71 |
72 |
73 |
74 |
80 |
82 |
88 |
89 |
90 |
91 |
93 |
94 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/mentorship ios/Views/Home/Home.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Home.swift
3 | // Created on 05/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct Home: View {
10 | var homeService: HomeService = HomeAPI()
11 | var profileService: ProfileService = ProfileAPI()
12 | @ObservedObject var homeViewModel = HomeViewModel()
13 | private var relationsData: UIHelper.HomeScreen.RelationsListData {
14 | return homeViewModel.relationsListData
15 | }
16 |
17 | var userFirstName: String {
18 |
19 | //Return just the first name
20 | if let editFullName = self.homeViewModel.userName?.capitalized {
21 | let trimmedFullName = editFullName.trimmingCharacters(in: .whitespaces)
22 | let userNameAsArray = trimmedFullName.components(separatedBy: " ")
23 | return userNameAsArray[0]
24 | }
25 | return ""
26 |
27 | }
28 |
29 | func useHomeService() {
30 | // fetch dashboard and map to home view model
31 | self.homeService.fetchDashboard { home in
32 | home.update(viewModel: self.homeViewModel)
33 | self.homeViewModel.isLoading = false
34 | }
35 |
36 | // if first time load, load profile too and use isLoading state (used to express in UI).
37 | if self.homeViewModel.firstTimeLoad {
38 | // set isLoading to true (expressed in UI)
39 | self.homeViewModel.isLoading = true
40 |
41 | // fetch profile and map to home view model.
42 | self.profileService.getProfile { profile in
43 | profile.update(viewModel: self.homeViewModel)
44 | // set first time load to false
45 | self.homeViewModel.firstTimeLoad = false
46 | }
47 | }
48 | }
49 |
50 | var body: some View {
51 | NavigationView {
52 | Form {
53 | //Top space
54 | Section {
55 | EmptyView()
56 | }
57 |
58 | //Relation dashboard list
59 | Section {
60 | ForEach(0 ..< relationsData.relationTitle.count) { index in
61 | NavigationLink(destination: RelationDetailList(
62 | index: index,
63 | navigationTitle: self.relationsData.relationTitle[index],
64 | homeViewModel: self.homeViewModel
65 | )) {
66 | RelationListCell(
67 | systemImageName: self.relationsData.relationImageName[index],
68 | imageColor: self.relationsData.relationImageColor[index],
69 | title: self.relationsData.relationTitle[index],
70 | count: self.relationsData.relationCount[index]
71 | )
72 | }
73 | .disabled(self.homeViewModel.isLoading)
74 | }
75 | }
76 |
77 | //Tasks to do list section
78 | TasksSection(tasks: homeViewModel.homeResponseData.tasksToDo, isToDoSection: true)
79 |
80 | //Tasks done list section
81 | TasksSection(tasks: homeViewModel.homeResponseData.tasksDone)
82 |
83 | }
84 | .environment(\.horizontalSizeClass, .regular)
85 | .navigationBarTitle("Welcome \(userFirstName)!")
86 | .navigationBarItems(trailing:
87 | NavigationLink(destination: ProfileSummary()) {
88 | Image(systemName: ImageNameConstants.SFSymbolConstants.profileIcon)
89 | .padding([.leading, .vertical])
90 | .font(.system(size: DesignConstants.Fonts.Size.navBarIcon))
91 | })
92 | .onAppear {
93 | self.useHomeService()
94 | }
95 | }
96 | }
97 | }
98 |
99 | struct Home_Previews: PreviewProvider {
100 | static var previews: some View {
101 | Home()
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/mentorship ios/Service/Networking/TaskCommentsAPI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TaskCommentsAPI.swift
3 | // Created on 28/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 | import Combine
9 |
10 | class TaskCommentsAPI: TaskCommentsService {
11 | private var cancellable: AnyCancellable?
12 | let urlSession: URLSession
13 |
14 | init(urlSession: URLSession = .shared) {
15 | self.urlSession = urlSession
16 | }
17 |
18 | func fetchTaskComments(
19 | reqID: Int,
20 | taskID: Int,
21 | completion: @escaping ([TaskCommentsModel.TaskCommentsResponse]) -> Void
22 | ) {
23 | //get auth token
24 | guard let token = try? KeychainManager.getToken() else {
25 | return
26 | }
27 |
28 | //api call
29 | cancellable = NetworkManager.callAPI(
30 | urlString: URLStringConstants.MentorshipRelation.getTaskComments(reqID: reqID, taskID: taskID),
31 | token: token,
32 | session: urlSession)
33 | .receive(on: RunLoop.main)
34 | .catch { _ in Just([CommentsNetworkResponse]()) }
35 | .sink { comments in
36 | var response = [TaskCommentsModel.TaskCommentsResponse]()
37 | for comment in comments {
38 | response.append(.init(id: comment.id, userID: comment.userID, creationDate: comment.creationDate, comment: comment.comment))
39 | }
40 | response = response.sorted()
41 | completion(response)
42 | }
43 | }
44 |
45 | func postTaskComment(
46 | reqID: Int,
47 | taskID: Int,
48 | commentData: TaskCommentsModel.PostCommentUploadData,
49 | completion: @escaping (TaskCommentsModel.MessageResponse) -> Void
50 | ) {
51 | //get auth token
52 | guard let token = try? KeychainManager.getToken() else {
53 | return
54 | }
55 |
56 | // encode upload data
57 | guard let uploadData = try? JSONEncoder().encode(commentData) else {
58 | return
59 | }
60 |
61 | //api call
62 | cancellable = NetworkManager.callAPI(
63 | urlString: URLStringConstants.MentorshipRelation.postTaskComment(reqID: reqID, taskID: taskID),
64 | httpMethod: "POST",
65 | uploadData: uploadData,
66 | token: token,
67 | session: urlSession)
68 | .receive(on: RunLoop.main)
69 | .catch { _ in Just(MessageResponse(message: LocalizableStringConstants.networkErrorString)) }
70 | .sink {
71 | let success = NetworkManager.responseCode == 201
72 | let response = TaskCommentsModel.MessageResponse(message: $0.message, success: success)
73 | completion(response)
74 | }
75 | }
76 |
77 | func reportComment(
78 | reqID: Int,
79 | taskID: Int,
80 | commentID: Int,
81 | completion: @escaping (TaskCommentsModel.MessageResponse) -> Void
82 | ) {
83 | //get auth token
84 | guard let token = try? KeychainManager.getToken() else {
85 | return
86 | }
87 |
88 | //api call
89 | cancellable = NetworkManager.callAPI(
90 | urlString: URLStringConstants.MentorshipRelation.reportTaskComment(reqID: reqID, taskID: taskID, commentID: commentID),
91 | httpMethod: "POST",
92 | token: token,
93 | session: urlSession)
94 | .receive(on: RunLoop.main)
95 | .catch { _ in Just(MessageResponse(message: LocalizableStringConstants.networkErrorString)) }
96 | .sink {
97 | let success = NetworkManager.responseCode == 200
98 | let response = TaskCommentsModel.MessageResponse(message: $0.message, success: success)
99 | completion(response)
100 | }
101 | }
102 |
103 | struct CommentsNetworkResponse: Decodable {
104 | let id: Int
105 | let userID: Int?
106 | let creationDate: Double?
107 | let comment: String?
108 |
109 | enum CodingKeys: String, CodingKey {
110 | case id, comment
111 | case userID = "user_id"
112 | case creationDate = "creation_date"
113 | }
114 | }
115 |
116 | struct MessageResponse: Decodable {
117 | let message: String?
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/mentorship ios/Views/Members/SendRequest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SendRequest.swift
3 | // Created on 09/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 | import Combine
9 |
10 | struct SendRequest: View {
11 | var membersService: MembersService = MembersAPI()
12 | @ObservedObject var membersViewModel = MembersViewModel()
13 | var memberID: Int
14 | var memberName: String
15 | @State private var pickerSelection = 1
16 | @State private var endDate = Date()
17 | @State private var notes = ""
18 | @State private var offsetValue: CGFloat = 0
19 | @Environment(\.presentationMode) var presentationMode
20 |
21 | // use service to send request
22 | func sendRequest() {
23 | // set inactivity to true
24 | self.membersViewModel.inActivity = true
25 |
26 | // set parameters
27 | let myID = ProfileViewModel().getProfile().id
28 | let endDateTimestamp = self.endDate.timeIntervalSince1970
29 | var menteeID = myID
30 | var mentorID = memberID
31 | if pickerSelection == 2 {
32 | menteeID = memberID
33 | mentorID = myID
34 | }
35 | // make request
36 | membersService.sendRequest(menteeID: menteeID, mentorID: mentorID, endDate: endDateTimestamp, notes: notes) { response in
37 | self.membersViewModel.inActivity = false
38 | self.membersViewModel.sendRequestResponseData = response
39 | }
40 | }
41 |
42 | var body: some View {
43 | NavigationView {
44 | List {
45 | //heading
46 | Section(header: Text("To \(memberName)").font(.title).fontWeight(.heavy)) {
47 | EmptyView()
48 | }
49 |
50 | //settings
51 | Section {
52 | Picker(selection: $pickerSelection, label: Text("My Role")) {
53 | Text(LocalizableStringConstants.mentee).tag(1)
54 | Text(LocalizableStringConstants.mentor).tag(2)
55 | }
56 |
57 | DatePicker(selection: $endDate, displayedComponents: .date) {
58 | Text(LocalizableStringConstants.endDate)
59 | }
60 |
61 | TextField(LocalizableStringConstants.notes, text: $notes)
62 | }
63 |
64 | //send button
65 | Section {
66 | Button(action: sendRequest) {
67 | Text(LocalizableStringConstants.send)
68 | }
69 | }
70 |
71 | //Activity indicator or error text
72 | if membersViewModel.inActivity || !(membersViewModel.sendRequestResponseData.message ?? "").isEmpty {
73 | Section {
74 | if membersViewModel.inActivity {
75 | ActivityIndicator(isAnimating: $membersViewModel.inActivity, style: .medium)
76 | } else if !membersViewModel.sendRequestResponseData.success {
77 | Text(membersViewModel.sendRequestResponseData.message ?? "")
78 | .modifier(ErrorText())
79 | }
80 | }
81 | .listRowBackground(DesignConstants.Colors.formBackgroundColor)
82 | }
83 | }
84 | .listStyle(GroupedListStyle())
85 | .navigationBarTitle(LocalizableStringConstants.relationRequest)
86 | .navigationBarItems(leading: Button(LocalizableStringConstants.cancel, action: {
87 | self.presentationMode.wrappedValue.dismiss()
88 | }))
89 | .alert(isPresented: $membersViewModel.sendRequestResponseData.success) {
90 | Alert(
91 | title: Text(LocalizableStringConstants.success),
92 | message: Text(membersViewModel.sendRequestResponseData.message ?? "Mentorship relation was sent successfully."),
93 | dismissButton: .cancel(Text(LocalizableStringConstants.okay), action: {
94 | self.presentationMode.wrappedValue.dismiss()
95 | self.membersViewModel.sendRequestResponseData.success = true
96 | })
97 | )
98 | }
99 | }
100 | }
101 | }
102 |
103 | struct SendRequest_Previews: PreviewProvider {
104 | static var previews: some View {
105 | SendRequest(memberID: 0, memberName: "demo name")
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/mentorship ios/Constants/LocalizableStringConstants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StringConstants.swift
3 | // Created on 04/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct LocalizableStringConstants {
10 | //Strictly keys - localized in localizable.strings file
11 | static let tncString = LocalizedStringKey("Terms and conditions")
12 | static let canBeBoth = LocalizedStringKey("Can be both")
13 | static let canBeMentee = LocalizedStringKey("Can be mentee")
14 | static let canBeMentor = LocalizedStringKey("Can be mentor")
15 | static let aboutText = LocalizedStringKey("About text")
16 | static let operationFail = LocalizedStringKey("Operation failed")
17 |
18 | // Not localizable. Reason: used in network requests, backend returns string.
19 | static let networkErrorString = "Network error. Please check your device network and try again."
20 | static let passwordsDoNotMatch = "Passwords do not match"
21 | static let you = "You"
22 |
23 | //Direct values for english. To be used as keys for other languages.
24 | static let noAccountText = LocalizedStringKey("Don't have an account?")
25 | static let availabilityText = LocalizedStringKey("Available to be a:")
26 | static let mentee = LocalizedStringKey("Mentee")
27 | static let mentor = LocalizedStringKey("Mentor")
28 | static let tasksDone = LocalizedStringKey("Tasks Done")
29 | static let tasksToDo = LocalizedStringKey("Tasks To Do")
30 | static let showEarlier = LocalizedStringKey("Show Earlier")
31 | static let enterComment = LocalizedStringKey("Enter comment")
32 | static let reportComment = LocalizedStringKey("Report This Comment")
33 | static let reportCommentMessage = LocalizedStringKey("Report comment message")
34 | static let actionsforComment = LocalizedStringKey("Actions for Comment")
35 | static let report = LocalizedStringKey("Report")
36 | static let endDate = LocalizedStringKey("End Date")
37 | static let notes = LocalizedStringKey("Notes")
38 | static let send = LocalizedStringKey("Send")
39 | static let cancel = LocalizedStringKey("Cancel")
40 | static let save = LocalizedStringKey("Save")
41 | static let okay = LocalizedStringKey("Okay")
42 | static let confirm = LocalizedStringKey("Confirm")
43 | static let success = LocalizedStringKey("Success")
44 | static let failure = LocalizedStringKey("Failure")
45 | static let profile = LocalizedStringKey("Profile")
46 | static let editProfile = LocalizedStringKey("Edit Profile")
47 | static let addTask = LocalizedStringKey("Add Task")
48 | static let markComplete = LocalizedStringKey("Mark as complete")
49 | static let relationRequest = LocalizedStringKey("Relation Request")
50 | static let notAvailable = LocalizedStringKey("Not available")
51 | static let privacyPolicy = LocalizedStringKey("Privacy Policy")
52 | static let termsOfUse = LocalizedStringKey("Terms of Use")
53 |
54 | // MARK: - Categorized
55 |
56 | //Keys to be used for Screen Names. (Direct values for English)
57 | struct ScreenNames {
58 | static let home = LocalizedStringKey("Home")
59 | static let relation = LocalizedStringKey("Relation")
60 | static let members = LocalizedStringKey("Members")
61 | static let settings = LocalizedStringKey("Settings")
62 | static let comments = LocalizedStringKey("Comments")
63 | }
64 |
65 | //Key to be used for relation request actions. (Direct values for English)
66 | struct RequestActions {
67 | static let accept = LocalizedStringKey("Accept")
68 | static let reject = LocalizedStringKey("Reject")
69 | static let delete = LocalizedStringKey("Delete")
70 | static let cancel = LocalizedStringKey("Withdraw")
71 | }
72 |
73 | //Keys to be used for profile attributes. (Direct values for English)
74 | enum ProfileKeys: LocalizedStringKey {
75 | case name = "Name"
76 | case username = "Username"
77 | case slackUsername = "Slack Username"
78 | case isMentor = "Is a Mentor"
79 | case needsMentor = "Needs a Mentor"
80 | case interests = "Interests"
81 | case bio = "Bio"
82 | case location = "Location"
83 | case occupation = "Occupation"
84 | case organization = "Organization"
85 | case skills = "Skills"
86 | case email = "Email"
87 | }
88 |
89 | //Keys to be used for text in ActivityWithText view. (Direct values for English)
90 | //Value string convention: All capitalized
91 | enum ActivityTextKeys: LocalizedStringKey {
92 | case updating = "UPDATING"
93 | case reporting = "REPORTING"
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/mentorship ios/Views/Registration/Login.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoginView.swift
3 | // Created on 01/06/20.
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct Login: View {
10 | var loginService: LoginService = LoginAPI()
11 | @State private var showSignUpPage: Bool = false
12 | @EnvironmentObject var loginViewModel: LoginViewModel
13 | @Environment(\.colorScheme) var colorScheme
14 |
15 | // Use service to login
16 | func login() {
17 | self.loginService.login(loginData: self.loginViewModel.loginData) { response in
18 | // update login view model
19 | self.loginViewModel.update(using: response)
20 | self.loginViewModel.inActivity = false
21 | }
22 | }
23 |
24 | var body: some View {
25 | VStack(spacing: DesignConstants.Form.Spacing.bigSpacing) {
26 | //top image of mentorship logo
27 | Image(ImageNameConstants.mentorshipLogoImageName)
28 | .resizable()
29 | .scaledToFit()
30 |
31 | //username and password text fields
32 | VStack(spacing: DesignConstants.Form.Spacing.smallSpacing) {
33 | TextField("Username/Email", text: $loginViewModel.loginData.username)
34 | .textFieldStyle(RoundFilledTextFieldStyle())
35 | .autocapitalization(.none)
36 | .keyboardType(.emailAddress)
37 |
38 | SecureField("Password", text: $loginViewModel.loginData.password)
39 | .textFieldStyle(RoundFilledTextFieldStyle())
40 | }
41 |
42 | //login button
43 | Button("Login") {
44 | // set inActivity to true (shows activity indicator)
45 | self.loginViewModel.inActivity = true
46 | self.login()
47 | }
48 | .buttonStyle(BigBoldButtonStyle(disabled: loginViewModel.loginDisabled))
49 | .disabled(loginViewModel.loginDisabled)
50 |
51 | //text and sign up button
52 | HStack(spacing: DesignConstants.Form.Spacing.minimalSpacing) {
53 | Text(LocalizableStringConstants.noAccountText)
54 |
55 | Button.init(action: { self.showSignUpPage.toggle() }) {
56 | Text("Signup")
57 | .foregroundColor(DesignConstants.Colors.defaultIndigoColor)
58 | }
59 | .sheet(isPresented: $showSignUpPage) {
60 | SignUp(isPresented: self.$showSignUpPage)
61 | }
62 | }
63 |
64 | //activity indicator or show user message text
65 | if self.loginViewModel.inActivity {
66 | ActivityIndicator(isAnimating: $loginViewModel.inActivity)
67 | } else if !(self.loginViewModel.loginResponseData.message?.isEmpty ?? true) {
68 | Text(self.loginViewModel.loginResponseData.message ?? "")
69 | .modifier(ErrorText())
70 | .fixedSize(horizontal: false, vertical: true)
71 | }
72 |
73 | VStack(spacing: DesignConstants.Spacing.smallSpacing) {
74 | // Divider for social sign in options
75 | ZStack {
76 | Divider()
77 | Text("OR").background(DesignConstants.Colors.primaryBackground)
78 | }
79 |
80 | // Social sign in buttons
81 | HStack {
82 | // Apple sign in Button. Adaptive to light/dark mode
83 | if colorScheme == .light {
84 | AppleSignInButton(dark: true).onTapGesture {
85 | self.loginViewModel.attemptAppleLogin()
86 | }
87 | } else {
88 | AppleSignInButton(dark: false).onTapGesture {
89 | self.loginViewModel.attemptAppleLogin()
90 | }
91 | }
92 |
93 | // Google sign in button
94 | GoogleSignInButton()
95 | .onTapGesture {
96 | SocialSignIn().attemptSignInGoogle()
97 | }
98 | }
99 | .frame(height: DesignConstants.Height.socialSignInButton)
100 | }
101 | }
102 | .onDisappear {
103 | self.loginViewModel.resetLogin()
104 | }
105 | .modifier(AllPadding())
106 | }
107 | }
108 |
109 | struct LoginView_Previews: PreviewProvider {
110 | static var previews: some View {
111 | Login()
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/mentorship ios/Service/Networking/MembersAPI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MembersAPI.swift
3 | // Created on 24/07/20
4 | // Created for AnitaB.org Mentorship-iOS
5 | //
6 |
7 | import Foundation
8 | import Combine
9 |
10 | class MembersAPI: MembersService {
11 | private var cancellable: AnyCancellable?
12 | let urlSession: URLSession
13 |
14 | init(urlSession: URLSession = .shared) {
15 | self.urlSession = urlSession
16 | }
17 |
18 | //Fetch Members
19 | func fetchMembers(pageToLoad: Int, perPage: Int, search: String, completion: @escaping ([MembersModel.MembersResponseData], Bool) -> Void) {
20 | //get auth token
21 | guard let token = try? KeychainManager.getToken() else {
22 | return
23 | }
24 |
25 | // api call
26 | cancellable = NetworkManager.callAPI(urlString: URLStringConstants.Users.members(page: pageToLoad, perPage: perPage, search: search), token: token, session: urlSession)
27 | .receive(on: RunLoop.main)
28 | .catch { _ in Just([MembersNetworkModel]()) }
29 | .sink {
30 | var membersResponse = [MembersModel.MembersResponseData]()
31 | for networkMember in $0 {
32 | // set member
33 | let member = MembersModel.MembersResponseData(
34 | id: networkMember.id,
35 | username: networkMember.username,
36 | name: networkMember.name,
37 | bio: networkMember.bio,
38 | location: networkMember.location,
39 | occupation: networkMember.occupation,
40 | organization: networkMember.organization,
41 | interests: networkMember.interests,
42 | skills: networkMember.skills,
43 | slackUsername: networkMember.slackUsername,
44 | needMentoring: networkMember.needMentoring,
45 | availableToMentor: networkMember.availableToMentor,
46 | isAvailable: networkMember.isAvailable)
47 | // append member to members response
48 | membersResponse.append(member)
49 | }
50 | // if count less than per page limit, list is full.
51 | let membersListFull = $0.count < perPage
52 | completion(membersResponse, membersListFull)
53 | }
54 | }
55 |
56 | //Send Request
57 | func sendRequest(menteeID: Int, mentorID: Int, endDate: Double, notes: String, completion: @escaping (MembersModel.SendRequestResponseData) -> Void) {
58 | //token
59 | guard let token = try? KeychainManager.getToken() else {
60 | return
61 | }
62 |
63 | //upload data
64 | let requestData = MembersModel.SendRequestUploadData(mentorID: mentorID, menteeID: menteeID, endDate: endDate, notes: notes)
65 | NSLog("A relation request was made to the server.")
66 | guard let uploadData = try? JSONEncoder().encode(requestData) else {
67 | return
68 | }
69 |
70 | //api call
71 | cancellable = NetworkManager.callAPI(urlString: URLStringConstants.MentorshipRelation.sendRequest, httpMethod: "POST", uploadData: uploadData, token: token, session: urlSession)
72 | .receive(on: RunLoop.main)
73 | .catch { _ in Just(SendRequestNetworkModel(message: LocalizableStringConstants.networkErrorString)) }
74 | .sink {
75 | let success = NetworkManager.responseCode == 201
76 | let responseData = MembersModel.SendRequestResponseData(message: $0.message, success: success)
77 | completion(responseData)
78 | }
79 | }
80 |
81 | struct MembersNetworkModel: Decodable {
82 | let id: Int
83 |
84 | let username: String?
85 | let name: String?
86 |
87 | let bio: String?
88 | let location: String?
89 | let occupation: String?
90 | let organization: String?
91 | let interests: String?
92 | let skills: String?
93 |
94 | let slackUsername: String?
95 | let needMentoring: Bool?
96 | let availableToMentor: Bool?
97 | let isAvailable: Bool?
98 |
99 | enum CodingKeys: String, CodingKey {
100 | case id, username, name, bio, location, occupation, organization, interests, skills
101 | case slackUsername = "slack_username"
102 | case needMentoring = "need_mentoring"
103 | case availableToMentor = "available_to_mentor"
104 | case isAvailable = "is_available"
105 | }
106 | }
107 |
108 | struct SendRequestNetworkModel: Decodable {
109 | let message: String?
110 | }
111 | }
112 |
--------------------------------------------------------------------------------