├── .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 | |![Login](https://github.com/anitab-org/mentorship-ios/blob/yugantarjain-patch-1/Screenshots/Screenshots/Login.png)|![SignUp](https://github.com/anitab-org/mentorship-ios/blob/yugantarjain-patch-1/Screenshots/Screenshots/SignUp.png)|![Home](https://github.com/anitab-org/mentorship-ios/blob/yugantarjain-patch-1/Screenshots/Screenshots/Home.png)| 6 | |Request Detail|Profile|Profile Editor| 7 | |![RequestDetail](https://github.com/anitab-org/mentorship-ios/blob/yugantarjain-patch-1/Screenshots/Screenshots/RequestDetail.png)|![Profile](https://github.com/anitab-org/mentorship-ios/blob/yugantarjain-patch-1/Screenshots/Screenshots/Profile.png)|![Editor](https://github.com/anitab-org/mentorship-ios/blob/yugantarjain-patch-1/Screenshots/Screenshots/ProfileEditor.png)| 8 | |Relation|Add Task|Mark as Complete| 9 | |![Relation](https://github.com/anitab-org/mentorship-ios/blob/yugantarjain-patch-1/Screenshots/Screenshots/Relation.png)|![AddTask](https://github.com/anitab-org/mentorship-ios/blob/yugantarjain-patch-1/Screenshots/Screenshots/AddTask.png)|![MarkComplete](https://github.com/anitab-org/mentorship-ios/blob/yugantarjain-patch-1/Screenshots/Screenshots/MarkComplete.png)| 10 | |Task Comments|Report Comment|Members List| 11 | |![TaskComments](https://github.com/anitab-org/mentorship-ios/blob/yugantarjain-patch-1/Screenshots/Screenshots/TaskComments.png)|![ReportComment](https://github.com/anitab-org/mentorship-ios/blob/yugantarjain-patch-1/Screenshots/Screenshots/ReportComment.png)|![Members](https://github.com/anitab-org/mentorship-ios/blob/yugantarjain-patch-1/Screenshots/Screenshots/Members.png)| 12 | |Members Search|Member Detail|Send Request| 13 | |![Search](https://github.com/anitab-org/mentorship-ios/blob/yugantarjain-patch-1/Screenshots/Screenshots/Search.png)|![MemberDetail](https://github.com/anitab-org/mentorship-ios/blob/yugantarjain-patch-1/Screenshots/Screenshots/MemberDetail.png)|![SendRequest](https://github.com/anitab-org/mentorship-ios/blob/yugantarjain-patch-1/Screenshots/Screenshots/SendRequest.png)| 14 | |Settings|About|Change Password| 15 | |![Settings](https://github.com/anitab-org/mentorship-ios/blob/yugantarjain-patch-1/Screenshots/Screenshots/Settings.png)|![About](https://github.com/anitab-org/mentorship-ios/blob/yugantarjain-patch-1/Screenshots/Screenshots/About.png)|![ChangePassword](https://github.com/anitab-org/mentorship-ios/blob/yugantarjain-patch-1/Screenshots/Screenshots/ChangePassword.png)| 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 | ![](https://github.com/anitab-org/mentorship-ios/blob/yugantarjain-patch-1/Screenshots/Showcase/HOME.001.jpeg) 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 | --------------------------------------------------------------------------------