diff --git a/Q-municate/Q-municate.xcodeproj/project.pbxproj b/Q-municate/Q-municate.xcodeproj/project.pbxproj new file mode 100644 index 000000000..f9c6ac515 --- /dev/null +++ b/Q-municate/Q-municate.xcodeproj/project.pbxproj @@ -0,0 +1,840 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 2C1094ED2B31D15400A02DCE /* EnterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C1094EC2B31D15400A02DCE /* EnterView.swift */; }; + 2C3562E32B2CA82000039D22 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3562E22B2CA82000039D22 /* User.swift */; }; + 2C4417F52B2717E200216796 /* Q_municateApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4417F42B2717E200216796 /* Q_municateApp.swift */; }; + 2C4417FC2B2717E300216796 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2C4417FB2B2717E300216796 /* Preview Assets.xcassets */; }; + 2C4418062B2717E300216796 /* Q_municateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4418052B2717E300216796 /* Q_municateTests.swift */; }; + 2C4418102B2717E400216796 /* Q_municateUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C44180F2B2717E400216796 /* Q_municateUITests.swift */; }; + 2C4418122B2717E400216796 /* Q_municateUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4418112B2717E400216796 /* Q_municateUITestsLaunchTests.swift */; }; + 2C4418602B27525600216796 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4418352B27525600216796 /* API.swift */; }; + 2C4418612B27525600216796 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2C4418372B27525600216796 /* Assets.xcassets */; }; + 2C4418622B27525600216796 /* PhoneCountryCodes.json in Resources */ = {isa = PBXBuildFile; fileRef = 2C4418382B27525600216796 /* PhoneCountryCodes.json */; }; + 2C4418632B27525600216796 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4418392B27525600216796 /* AppDelegate.swift */; }; + 2C4418742B27525600216796 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C44184C2B27525600216796 /* Errors.swift */; }; + 2C44188C2B279F5D00216796 /* EnterPhoneNumberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C44188B2B279F5D00216796 /* EnterPhoneNumberView.swift */; }; + 2C44188F2B27A1B500216796 /* AppTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C44188E2B27A1B500216796 /* AppTheme.swift */; }; + 2C4418922B27A6EC00216796 /* EnterPhoneNumberSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4418912B27A6EC00216796 /* EnterPhoneNumberSettings.swift */; }; + 2C99FA842B38AF8700C60BAE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2C99FA832B38AF8700C60BAE /* LaunchScreen.storyboard */; }; + 2CAA7AED2B29E53A000FB3A3 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 2CAA7AEC2B29E53A000FB3A3 /* Localizable.xcstrings */; }; + 2CB29A922B6682CF00BCFB40 /* EnterScreenSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB29A912B6682CF00BCFB40 /* EnterScreenSettings.swift */; }; + 2CBF214F2B28AA88008BF463 /* EnterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBF214E2B28AA88008BF463 /* EnterViewModel.swift */; }; + 2CBF21512B28AB5D008BF463 /* CountryPhoneCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBF21502B28AB5D008BF463 /* CountryPhoneCode.swift */; }; + 2CBF21532B28B48F008BF463 /* CountryScreenSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBF21522B28B48F008BF463 /* CountryScreenSettings.swift */; }; + 2CBF21552B290A80008BF463 /* SelectCountryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBF21542B290A80008BF463 /* SelectCountryListView.swift */; }; + 2CC067042B3DA6C8003CD9EE /* FirebaseAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC067032B3DA6C8003CD9EE /* FirebaseAPI.swift */; }; + 2CC067062B3DA6ED003CD9EE /* QuickbloxAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC067052B3DA6ED003CD9EE /* QuickbloxAPI.swift */; }; + 2CC5F3DF2B2B210D00493BB1 /* VerifyPhoneNumberScreenSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC5F3DE2B2B210D00493BB1 /* VerifyPhoneNumberScreenSettings.swift */; }; + 2CC5F3E12B2B2FA800493BB1 /* CreateProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC5F3E02B2B2FA800493BB1 /* CreateProfileView.swift */; }; + 2CC5F3E32B2B32C200493BB1 /* ProfileScreenSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC5F3E22B2B32C200493BB1 /* ProfileScreenSettings.swift */; }; + 2CD477E02B6A8D070073959B /* QuickBloxUIKit in Frameworks */ = {isa = PBXBuildFile; productRef = 2CD477DF2B6A8D070073959B /* QuickBloxUIKit */; }; + 2CDE3D6D2B51629200DB1EBB /* FirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 2CDE3D6C2B51629200DB1EBB /* FirebaseAuth */; }; + 2CEDA5AC2B29F11D00B8167B /* TermsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEDA5AB2B29F11D00B8167B /* TermsView.swift */; }; + 2CEDA5AE2B2A04D900B8167B /* VerifyPhoneNumberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEDA5AD2B2A04D900B8167B /* VerifyPhoneNumberView.swift */; }; + 2CFD6C092B28A11300C306AE /* CountryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFD6C082B28A11300C306AE /* CountryView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 2C4418022B2717E300216796 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2C4417E92B2717E200216796 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2C4417F02B2717E200216796; + remoteInfo = "Q-municate"; + }; + 2C44180C2B2717E400216796 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2C4417E92B2717E200216796 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2C4417F02B2717E200216796; + remoteInfo = "Q-municate"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 2C1094EC2B31D15400A02DCE /* EnterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterView.swift; sourceTree = ""; }; + 2C3562E22B2CA82000039D22 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + 2C4417F12B2717E200216796 /* Q-municate.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Q-municate.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 2C4417F42B2717E200216796 /* Q_municateApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Q_municateApp.swift; sourceTree = ""; }; + 2C4417FB2B2717E300216796 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 2C4418012B2717E300216796 /* Q-municateTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Q-municateTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 2C4418052B2717E300216796 /* Q_municateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Q_municateTests.swift; sourceTree = ""; }; + 2C44180B2B2717E400216796 /* Q-municateUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Q-municateUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 2C44180F2B2717E400216796 /* Q_municateUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Q_municateUITests.swift; sourceTree = ""; }; + 2C4418112B2717E400216796 /* Q_municateUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Q_municateUITestsLaunchTests.swift; sourceTree = ""; }; + 2C4418222B271B4F00216796 /* Q-municate.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Q-municate.entitlements"; sourceTree = ""; }; + 2C4418252B2734F300216796 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2C4418352B27525600216796 /* API.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; + 2C4418372B27525600216796 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 2C4418382B27525600216796 /* PhoneCountryCodes.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = PhoneCountryCodes.json; sourceTree = ""; }; + 2C4418392B27525600216796 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 2C44184C2B27525600216796 /* Errors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; + 2C44188B2B279F5D00216796 /* EnterPhoneNumberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterPhoneNumberView.swift; sourceTree = ""; }; + 2C44188E2B27A1B500216796 /* AppTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppTheme.swift; sourceTree = ""; }; + 2C4418912B27A6EC00216796 /* EnterPhoneNumberSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterPhoneNumberSettings.swift; sourceTree = ""; }; + 2C99FA832B38AF8700C60BAE /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + 2CAA7AEC2B29E53A000FB3A3 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; + 2CB29A912B6682CF00BCFB40 /* EnterScreenSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterScreenSettings.swift; sourceTree = ""; }; + 2CBF214E2B28AA88008BF463 /* EnterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterViewModel.swift; sourceTree = ""; }; + 2CBF21502B28AB5D008BF463 /* CountryPhoneCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryPhoneCode.swift; sourceTree = ""; }; + 2CBF21522B28B48F008BF463 /* CountryScreenSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryScreenSettings.swift; sourceTree = ""; }; + 2CBF21542B290A80008BF463 /* SelectCountryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCountryListView.swift; sourceTree = ""; }; + 2CC067032B3DA6C8003CD9EE /* FirebaseAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseAPI.swift; sourceTree = ""; }; + 2CC067052B3DA6ED003CD9EE /* QuickbloxAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickbloxAPI.swift; sourceTree = ""; }; + 2CC5F3DE2B2B210D00493BB1 /* VerifyPhoneNumberScreenSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifyPhoneNumberScreenSettings.swift; sourceTree = ""; }; + 2CC5F3E02B2B2FA800493BB1 /* CreateProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileView.swift; sourceTree = ""; }; + 2CC5F3E22B2B32C200493BB1 /* ProfileScreenSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileScreenSettings.swift; sourceTree = ""; }; + 2CEDA5AB2B29F11D00B8167B /* TermsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsView.swift; sourceTree = ""; }; + 2CEDA5AD2B2A04D900B8167B /* VerifyPhoneNumberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifyPhoneNumberView.swift; sourceTree = ""; }; + 2CFD6C082B28A11300C306AE /* CountryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryView.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2C4417EE2B2717E200216796 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2CD477E02B6A8D070073959B /* QuickBloxUIKit in Frameworks */, + 2CDE3D6D2B51629200DB1EBB /* FirebaseAuth in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2C4417FE2B2717E300216796 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2C4418082B2717E400216796 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2C4417E82B2717E200216796 = { + isa = PBXGroup; + children = ( + 2C4417F32B2717E200216796 /* Q-municate */, + 2C4418042B2717E300216796 /* Q-municateTests */, + 2C44180E2B2717E400216796 /* Q-municateUITests */, + 2C4417F22B2717E200216796 /* Products */, + ); + sourceTree = ""; + }; + 2C4417F22B2717E200216796 /* Products */ = { + isa = PBXGroup; + children = ( + 2C4417F12B2717E200216796 /* Q-municate.app */, + 2C4418012B2717E300216796 /* Q-municateTests.xctest */, + 2C44180B2B2717E400216796 /* Q-municateUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 2C4417F32B2717E200216796 /* Q-municate */ = { + isa = PBXGroup; + children = ( + 2C4418452B27525600216796 /* Model */, + 2C4418342B27525600216796 /* API */, + 2C4418362B27525600216796 /* Resources */, + 2C44184D2B27525600216796 /* View */, + 2C44182B2B27525600216796 /* ViewModel */, + 2C4418232B27347600216796 /* Supporting Files */, + 2C4417F42B2717E200216796 /* Q_municateApp.swift */, + 2C4417FA2B2717E300216796 /* Preview Content */, + ); + path = "Q-municate"; + sourceTree = ""; + }; + 2C4417FA2B2717E300216796 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 2C4417FB2B2717E300216796 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 2C4418042B2717E300216796 /* Q-municateTests */ = { + isa = PBXGroup; + children = ( + 2C4418052B2717E300216796 /* Q_municateTests.swift */, + ); + path = "Q-municateTests"; + sourceTree = ""; + }; + 2C44180E2B2717E400216796 /* Q-municateUITests */ = { + isa = PBXGroup; + children = ( + 2C44180F2B2717E400216796 /* Q_municateUITests.swift */, + 2C4418112B2717E400216796 /* Q_municateUITestsLaunchTests.swift */, + ); + path = "Q-municateUITests"; + sourceTree = ""; + }; + 2C4418232B27347600216796 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 2C4418222B271B4F00216796 /* Q-municate.entitlements */, + 2C4418252B2734F300216796 /* Info.plist */, + ); + path = "Supporting Files"; + sourceTree = ""; + }; + 2C44182B2B27525600216796 /* ViewModel */ = { + isa = PBXGroup; + children = ( + 2CBF214E2B28AA88008BF463 /* EnterViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 2C4418342B27525600216796 /* API */ = { + isa = PBXGroup; + children = ( + 2C4418352B27525600216796 /* API.swift */, + 2CC067052B3DA6ED003CD9EE /* QuickbloxAPI.swift */, + 2CC067032B3DA6C8003CD9EE /* FirebaseAPI.swift */, + ); + path = API; + sourceTree = ""; + }; + 2C4418362B27525600216796 /* Resources */ = { + isa = PBXGroup; + children = ( + 2C4418902B27A6C500216796 /* Settings */, + 2C44188D2B27A1B500216796 /* AppTheme */, + 2C4418372B27525600216796 /* Assets.xcassets */, + 2C4418382B27525600216796 /* PhoneCountryCodes.json */, + 2CAA7AEC2B29E53A000FB3A3 /* Localizable.xcstrings */, + 2C4418392B27525600216796 /* AppDelegate.swift */, + ); + path = Resources; + sourceTree = ""; + }; + 2C4418452B27525600216796 /* Model */ = { + isa = PBXGroup; + children = ( + 2C44184C2B27525600216796 /* Errors.swift */, + 2CBF21502B28AB5D008BF463 /* CountryPhoneCode.swift */, + 2C3562E22B2CA82000039D22 /* User.swift */, + ); + path = Model; + sourceTree = ""; + }; + 2C44184D2B27525600216796 /* View */ = { + isa = PBXGroup; + children = ( + 2C99FA832B38AF8700C60BAE /* LaunchScreen.storyboard */, + 2CEDA5AB2B29F11D00B8167B /* TermsView.swift */, + 2CFD6C082B28A11300C306AE /* CountryView.swift */, + 2CBF21542B290A80008BF463 /* SelectCountryListView.swift */, + 2CEDA5AD2B2A04D900B8167B /* VerifyPhoneNumberView.swift */, + 2CC5F3E02B2B2FA800493BB1 /* CreateProfileView.swift */, + 2C44188B2B279F5D00216796 /* EnterPhoneNumberView.swift */, + 2C1094EC2B31D15400A02DCE /* EnterView.swift */, + ); + path = View; + sourceTree = ""; + }; + 2C44188D2B27A1B500216796 /* AppTheme */ = { + isa = PBXGroup; + children = ( + 2C44188E2B27A1B500216796 /* AppTheme.swift */, + ); + path = AppTheme; + sourceTree = ""; + }; + 2C4418902B27A6C500216796 /* Settings */ = { + isa = PBXGroup; + children = ( + 2C4418912B27A6EC00216796 /* EnterPhoneNumberSettings.swift */, + 2CC5F3DE2B2B210D00493BB1 /* VerifyPhoneNumberScreenSettings.swift */, + 2CB29A912B6682CF00BCFB40 /* EnterScreenSettings.swift */, + 2CC5F3E22B2B32C200493BB1 /* ProfileScreenSettings.swift */, + 2CBF21522B28B48F008BF463 /* CountryScreenSettings.swift */, + ); + path = Settings; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 2C4417F02B2717E200216796 /* Q-municate */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2C4418152B2717E400216796 /* Build configuration list for PBXNativeTarget "Q-municate" */; + buildPhases = ( + 2C4417ED2B2717E200216796 /* Sources */, + 2C4417EE2B2717E200216796 /* Frameworks */, + 2C4417EF2B2717E200216796 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Q-municate"; + packageProductDependencies = ( + 2CDE3D6C2B51629200DB1EBB /* FirebaseAuth */, + 2CD477DF2B6A8D070073959B /* QuickBloxUIKit */, + ); + productName = "Q-municate"; + productReference = 2C4417F12B2717E200216796 /* Q-municate.app */; + productType = "com.apple.product-type.application"; + }; + 2C4418002B2717E300216796 /* Q-municateTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2C4418182B2717E400216796 /* Build configuration list for PBXNativeTarget "Q-municateTests" */; + buildPhases = ( + 2C4417FD2B2717E300216796 /* Sources */, + 2C4417FE2B2717E300216796 /* Frameworks */, + 2C4417FF2B2717E300216796 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2C4418032B2717E300216796 /* PBXTargetDependency */, + ); + name = "Q-municateTests"; + productName = "Q-municateTests"; + productReference = 2C4418012B2717E300216796 /* Q-municateTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 2C44180A2B2717E400216796 /* Q-municateUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2C44181B2B2717E400216796 /* Build configuration list for PBXNativeTarget "Q-municateUITests" */; + buildPhases = ( + 2C4418072B2717E400216796 /* Sources */, + 2C4418082B2717E400216796 /* Frameworks */, + 2C4418092B2717E400216796 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2C44180D2B2717E400216796 /* PBXTargetDependency */, + ); + name = "Q-municateUITests"; + productName = "Q-municateUITests"; + productReference = 2C44180B2B2717E400216796 /* Q-municateUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 2C4417E92B2717E200216796 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1510; + TargetAttributes = { + 2C4417F02B2717E200216796 = { + CreatedOnToolsVersion = 15.0.1; + }; + 2C4418002B2717E300216796 = { + CreatedOnToolsVersion = 15.0.1; + TestTargetID = 2C4417F02B2717E200216796; + }; + 2C44180A2B2717E400216796 = { + CreatedOnToolsVersion = 15.0.1; + TestTargetID = 2C4417F02B2717E200216796; + }; + }; + }; + buildConfigurationList = 2C4417EC2B2717E200216796 /* Build configuration list for PBXProject "Q-municate" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + es, + Base, + ); + mainGroup = 2C4417E82B2717E200216796; + packageReferences = ( + 2CDE3D6B2B51629200DB1EBB /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, + 2CD477DE2B6A8D060073959B /* XCRemoteSwiftPackageReference "ios-ui-kit" */, + ); + productRefGroup = 2C4417F22B2717E200216796 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2C4417F02B2717E200216796 /* Q-municate */, + 2C4418002B2717E300216796 /* Q-municateTests */, + 2C44180A2B2717E400216796 /* Q-municateUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 2C4417EF2B2717E200216796 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2C4417FC2B2717E300216796 /* Preview Assets.xcassets in Resources */, + 2CAA7AED2B29E53A000FB3A3 /* Localizable.xcstrings in Resources */, + 2C99FA842B38AF8700C60BAE /* LaunchScreen.storyboard in Resources */, + 2C4418622B27525600216796 /* PhoneCountryCodes.json in Resources */, + 2C4418612B27525600216796 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2C4417FF2B2717E300216796 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2C4418092B2717E400216796 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 2C4417ED2B2717E200216796 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2CC5F3E32B2B32C200493BB1 /* ProfileScreenSettings.swift in Sources */, + 2CC067042B3DA6C8003CD9EE /* FirebaseAPI.swift in Sources */, + 2C4418632B27525600216796 /* AppDelegate.swift in Sources */, + 2C44188F2B27A1B500216796 /* AppTheme.swift in Sources */, + 2C1094ED2B31D15400A02DCE /* EnterView.swift in Sources */, + 2CB29A922B6682CF00BCFB40 /* EnterScreenSettings.swift in Sources */, + 2CC5F3DF2B2B210D00493BB1 /* VerifyPhoneNumberScreenSettings.swift in Sources */, + 2CC067062B3DA6ED003CD9EE /* QuickbloxAPI.swift in Sources */, + 2CBF21512B28AB5D008BF463 /* CountryPhoneCode.swift in Sources */, + 2CBF214F2B28AA88008BF463 /* EnterViewModel.swift in Sources */, + 2CC5F3E12B2B2FA800493BB1 /* CreateProfileView.swift in Sources */, + 2CBF21532B28B48F008BF463 /* CountryScreenSettings.swift in Sources */, + 2C4418602B27525600216796 /* API.swift in Sources */, + 2CBF21552B290A80008BF463 /* SelectCountryListView.swift in Sources */, + 2CEDA5AE2B2A04D900B8167B /* VerifyPhoneNumberView.swift in Sources */, + 2C3562E32B2CA82000039D22 /* User.swift in Sources */, + 2C44188C2B279F5D00216796 /* EnterPhoneNumberView.swift in Sources */, + 2C4418922B27A6EC00216796 /* EnterPhoneNumberSettings.swift in Sources */, + 2C4417F52B2717E200216796 /* Q_municateApp.swift in Sources */, + 2CEDA5AC2B29F11D00B8167B /* TermsView.swift in Sources */, + 2CFD6C092B28A11300C306AE /* CountryView.swift in Sources */, + 2C4418742B27525600216796 /* Errors.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2C4417FD2B2717E300216796 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2C4418062B2717E300216796 /* Q_municateTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2C4418072B2717E400216796 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2C4418122B2717E400216796 /* Q_municateUITestsLaunchTests.swift in Sources */, + 2C4418102B2717E400216796 /* Q_municateUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 2C4418032B2717E300216796 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2C4417F02B2717E200216796 /* Q-municate */; + targetProxy = 2C4418022B2717E300216796 /* PBXContainerItemProxy */; + }; + 2C44180D2B2717E400216796 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2C4417F02B2717E200216796 /* Q-municate */; + targetProxy = 2C44180C2B2717E400216796 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 2C4418132B2717E400216796 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 2C4418142B2717E400216796 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 2C4418162B2717E400216796 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "Q-municate/Supporting Files/Q-municate.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Q-municate/Preview Content\""; + DEVELOPMENT_TEAM = 8885H5G2YX; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Q-municate/Supporting Files/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "Q-municate"; + INFOPLIST_KEY_NSCameraUsageDescription = "Taking pictures, video calls"; + INFOPLIST_KEY_NSContactsUsageDescription = "Saving images for contacts"; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Location for messaging"; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "This app wants to record sound."; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "This app wants to save pictures & videos to your library."; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app wants to use your picture & video library."; + INFOPLIST_KEY_NSSiriUsageDescription = Messaging; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIRequiredDeviceCapabilities = armv7; + INFOPLIST_KEY_UIRequiresFullScreen = YES; + INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 4.0.0; + PRODUCT_BUNDLE_IDENTIFIER = com.quickblox.qmunicate; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2C4418172B2717E400216796 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "Q-municate/Supporting Files/Q-municate.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Q-municate/Preview Content\""; + DEVELOPMENT_TEAM = 8885H5G2YX; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Q-municate/Supporting Files/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "Q-municate"; + INFOPLIST_KEY_NSCameraUsageDescription = "Taking pictures, video calls"; + INFOPLIST_KEY_NSContactsUsageDescription = "Saving images for contacts"; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Location for messaging"; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "This app wants to record sound."; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "This app wants to save pictures & videos to your library."; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app wants to use your picture & video library."; + INFOPLIST_KEY_NSSiriUsageDescription = Messaging; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIRequiredDeviceCapabilities = armv7; + INFOPLIST_KEY_UIRequiresFullScreen = YES; + INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 4.0.0; + PRODUCT_BUNDLE_IDENTIFIER = com.quickblox.qmunicate; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 2C4418192B2717E400216796 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8885H5G2YX; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.quickblox.Q-municateTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Q-municate.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Q-municate"; + }; + name = Debug; + }; + 2C44181A2B2717E400216796 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8885H5G2YX; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.quickblox.Q-municateTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Q-municate.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Q-municate"; + }; + name = Release; + }; + 2C44181C2B2717E400216796 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8885H5G2YX; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.quickblox.Q-municateUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "Q-municate"; + }; + name = Debug; + }; + 2C44181D2B2717E400216796 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8885H5G2YX; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.quickblox.Q-municateUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "Q-municate"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2C4417EC2B2717E200216796 /* Build configuration list for PBXProject "Q-municate" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2C4418132B2717E400216796 /* Debug */, + 2C4418142B2717E400216796 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2C4418152B2717E400216796 /* Build configuration list for PBXNativeTarget "Q-municate" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2C4418162B2717E400216796 /* Debug */, + 2C4418172B2717E400216796 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2C4418182B2717E400216796 /* Build configuration list for PBXNativeTarget "Q-municateTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2C4418192B2717E400216796 /* Debug */, + 2C44181A2B2717E400216796 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2C44181B2B2717E400216796 /* Build configuration list for PBXNativeTarget "Q-municateUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2C44181C2B2717E400216796 /* Debug */, + 2C44181D2B2717E400216796 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 2CD477DE2B6A8D060073959B /* XCRemoteSwiftPackageReference "ios-ui-kit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/QuickBlox/ios-ui-kit.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.3.4; + }; + }; + 2CDE3D6B2B51629200DB1EBB /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 10.19.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 2CD477DF2B6A8D070073959B /* QuickBloxUIKit */ = { + isa = XCSwiftPackageProductDependency; + package = 2CD477DE2B6A8D060073959B /* XCRemoteSwiftPackageReference "ios-ui-kit" */; + productName = QuickBloxUIKit; + }; + 2CDE3D6C2B51629200DB1EBB /* FirebaseAuth */ = { + isa = XCSwiftPackageProductDependency; + package = 2CDE3D6B2B51629200DB1EBB /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseAuth; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 2C4417E92B2717E200216796 /* Project object */; +} diff --git a/Q-municate/Q-municate.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Q-municate/Q-municate.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/Q-municate/Q-municate.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Q-municate/Q-municate.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Q-municate/Q-municate.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/Q-municate/Q-municate.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Q-municate/Q-municate.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Q-municate/Q-municate.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..513e36ce6 --- /dev/null +++ b/Q-municate/Q-municate.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,167 @@ +{ + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "bfc0b6f81adc06ce5121eb23f628473638d67c5c", + "version" : "1.2022062300.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "3e464dad87dad2d29bb29a97836789bf0f8f67d2", + "version" : "10.18.1" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk.git", + "state" : { + "revision" : "b880ec8ec927a838c51c12862c6222c30d7097d7", + "version" : "10.20.0" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "ceec9f28dea12b7cf3dabf18b5ed7621c88fd4aa", + "version" : "10.20.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "a732a4b47f59e4f725a2ea10f0c77e93a7131117", + "version" : "9.3.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "bc27fad73504f3d4af235de451f02ee22586ebd3", + "version" : "7.12.1" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "a673bc2937fbe886dd1f99c401b01b6d977a9c98", + "version" : "1.49.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "76135c9f4e1ac85459d5fec61b6f76ac47ab3a4c", + "version" : "3.3.1" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "2d12673670417654f08f5f90fdd62926dc3a2648", + "version" : "100.0.0" + } + }, + { + "identity" : "ios-ai-answer-assistant", + "kind" : "remoteSourceControl", + "location" : "https://github.com/QuickBlox/ios-ai-answer-assistant.git", + "state" : { + "revision" : "83da3d2846129853ad4a626205f7fea7edc933c2", + "version" : "2.0.0" + } + }, + { + "identity" : "ios-ai-rephrase", + "kind" : "remoteSourceControl", + "location" : "https://github.com/QuickBlox/ios-ai-rephrase.git", + "state" : { + "revision" : "633d1ce0219d0e48dd5773716e631233a5abe017", + "version" : "2.0.0" + } + }, + { + "identity" : "ios-ai-translate", + "kind" : "remoteSourceControl", + "location" : "https://github.com/QuickBlox/ios-ai-translate.git", + "state" : { + "revision" : "52a058215c03fa101c6810b692b80be2b07fc8ed", + "version" : "2.0.0" + } + }, + { + "identity" : "ios-quickblox-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/QuickBlox/ios-quickblox-sdk", + "state" : { + "revision" : "54fa973bf6a788529ae1b39ba5e301db94218385", + "version" : "2.19.0" + } + }, + { + "identity" : "ios-ui-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/QuickBlox/ios-ui-kit.git", + "state" : { + "revision" : "cf4ee2b6c3330d66be3be88aaf2425bd561c1497", + "version" : "0.3.4" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "9d108e9112aa1d65ce508facf804674546116d9c", + "version" : "1.22.3" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692", + "version" : "2.30909.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "e70e889c0196c76d22759eb50d6a0270ca9f1d9e", + "version" : "2.3.1" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "65e8f29b2d63c4e38e736b25c27b83e012159be8", + "version" : "1.25.2" + } + } + ], + "version" : 2 +} diff --git a/Q-municate/Q-municate.xcodeproj/xcshareddata/xcschemes/Q-municate.xcscheme b/Q-municate/Q-municate.xcodeproj/xcshareddata/xcschemes/Q-municate.xcscheme new file mode 100644 index 000000000..d34e6ec75 --- /dev/null +++ b/Q-municate/Q-municate.xcodeproj/xcshareddata/xcschemes/Q-municate.xcscheme @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Q-municate/Q-municate/API/API.swift b/Q-municate/Q-municate/API/API.swift new file mode 100644 index 000000000..aa2a5ecb2 --- /dev/null +++ b/Q-municate/Q-municate/API/API.swift @@ -0,0 +1,146 @@ +// +// API.swift +// Q-municate +// +// Created by Injoit on 11.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import SwiftUI + +struct APIConstant { + static let appID = "" +} + +struct API { + private let firebase: FirebaseAPI = FirebaseAPI() + private let quickblox: QuickbloxAPI = QuickbloxAPI() + + enum AppVersionCheckError: Error { + case invalidResponse, invalidBundleInfo + } + + @discardableResult + func isUpdateAvailable(completion: @escaping (_ lastVersion: String?, _ error: Error?) -> Void) throws -> URLSessionDataTask { + guard let info = Bundle.main.infoDictionary, + let currentVersion = info["CFBundleShortVersionString"] as? String, + let identifier = info["CFBundleIdentifier"] as? String, + let url = URL(string: "http://itunes.apple.com/lookup?bundleId=\(identifier)") else { + throw AppVersionCheckError.invalidBundleInfo + } + + let request = URLRequest(url: url, cachePolicy: URLRequest.CachePolicy.reloadIgnoringLocalCacheData) + + let task = URLSession.shared.dataTask(with: request) { (data, response, error) in + do { + if let error = error { throw error } + + guard let data = data else { throw AppVersionCheckError.invalidResponse } + + let json = try JSONSerialization.jsonObject(with: data, options: [.allowFragments]) as? [String: Any] + + guard let result = (json?["results"] as? [Any])?.first as? [String: Any], + let lastVersion = result["version"] as? String else { + throw AppVersionCheckError.invalidResponse + } + completion(lastVersion > currentVersion ? lastVersion : nil, nil) + } catch { + completion(nil, error) + } + } + + task.resume() + return task + } + + func openAppStore() { + if let url = URL(string: "https://itunes.apple.com/us/app/apple-store/id\(APIConstant.appID)"), + UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } + + func verifyPhoneNumber(_ phoneNumber: String, completion: @escaping (Error?) -> Void) { + firebase.verify(phoneNumber: phoneNumber) { error in + completion(error) + } + } + + func verifyCode(_ verificationCode: String, completion: @escaping (_ user: User?, _ error: Error?) -> Void) { + firebase.verify(code: verificationCode) { projectID, accessToken, error in + guard let projectID = projectID, + let accessToken = accessToken else { + completion(nil, error) + return + } + quickblox.logInWithFirebase(projectID, accessToken: accessToken) { user, error in + if let user { + completion(user, nil) + } else { + completion(nil, error) + } + } + } + } + + func autoLogin(with completion: @escaping (_ user: User?, _ error: Error?) -> Void) { + firebase.autoLogin { projectID, accessToken, error in + guard let projectID = projectID, + let accessToken = accessToken else { + completion(nil, error) + return + } + quickblox.logInWithFirebase(projectID, accessToken: accessToken) { user, error in + if let user { + completion(user, nil) + } else { + completion(nil, error) + } + } + } + } + + func logOut(_ completion: @escaping (Error?) -> Void) { + quickblox.disconnect { error in + completion(error) + } + } + + func updateUser(_ update: UpdateUserParameters, completion: @escaping (_ user: User?, _ error: Error?) -> Void) { + quickblox.updateUser(update) { user, error in + if let error { + completion(nil, error) + } else { + completion(user, nil) + } + } + } + + func getAvatar(_ blobId: UInt, completion: @escaping (_ avatar: UIImage?, _ error: Error?) -> Void) { + quickblox.getAvatar(blobId) { avatar, error in + if let error { + completion(nil, error) + } else { + completion(avatar, nil) + } + } + } + + func deleteAvatar(_ blobId: UInt, completion: @escaping (_ error: Error?) -> Void) { + quickblox.deleteAvatar(blobId) { error in + if let error { + completion(error) + } else { + completion(nil) + } + } + } + + func configure() { + quickblox.configure() + } + + func currentUser() -> User? { + return quickblox.currentUser() + } +} diff --git a/Q-municate/Q-municate/API/FirebaseAPI.swift b/Q-municate/Q-municate/API/FirebaseAPI.swift new file mode 100644 index 000000000..b8242c77d --- /dev/null +++ b/Q-municate/Q-municate/API/FirebaseAPI.swift @@ -0,0 +1,79 @@ +// +// FirebaseAPI.swift +// Q-municate +// +// Created by Injoit on 28.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import Foundation +import FirebaseAuth + +struct FirebaseAPIConstant { + static let verificationIDKey = "kVerificationID" + static let countryPhoneCodeKey = "kCountryPhoneCode" +} + + +struct FirebaseAPI { + + func verify(phoneNumber: String, completion: @escaping (Error?) -> Void) { + PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate: nil) { (verificationID, error) in + if let error = error { + completion(error) + return + } + if let verificationID { + UserDefaults.standard.set(verificationID, forKey: FirebaseAPIConstant.verificationIDKey) + completion(nil) + } + } + } + + func verify(code: String, completion: @escaping (_ projectID: String?, + _ accessToken: String?, + _ error: Error?) -> Void) { + guard let verificationID = UserDefaults.standard.string(forKey: FirebaseAPIConstant.verificationIDKey) else { + completion(nil, nil, AuthError.noVerificationID) + return + } + + let credential = PhoneAuthProvider.provider().credential(withVerificationID: verificationID, verificationCode: code) + + Auth.auth().signIn(with: credential) { (authResult, error) in + if let error { + completion(nil, nil, error) + } + if let authResult { + authResult.user.getIDToken { token, error in + if let error { + completion(nil, nil, error) + } + if let token, let projectID = Auth.auth().app?.options.projectID { + completion(projectID, token, nil) + } else { + completion(nil, nil, error) + } + } + } + } + } + + func autoLogin(with completion: @escaping (_ projectID: String?, + _ accessToken: String?, + _ error: Error?) -> Void) { + guard let phoneUser = Auth.auth().currentUser else { + completion(nil, nil, nil) + return } + phoneUser.getIDToken { token, error in + if let error { + completion(nil, nil, error) + } + if let token, let projectID = Auth.auth().app?.options.projectID { + completion(projectID, token, nil) + } else { + completion(nil, nil, error) + } + } + } +} diff --git a/Q-municate/Q-municate/API/QuickbloxAPI.swift b/Q-municate/Q-municate/API/QuickbloxAPI.swift new file mode 100644 index 000000000..1b66cc51e --- /dev/null +++ b/Q-municate/Q-municate/API/QuickbloxAPI.swift @@ -0,0 +1,168 @@ +// +// QuickbloxAPI.swift +// Q-municate +// +// Created by Injoit on 28.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import SwiftUI +import Quickblox + +struct QuickbloxAPI { + + func logInWithFirebase(_ projectID: String, + accessToken: String, + completion: @escaping (_ user: User?, _ error: Error?) -> Void) { + QBRequest.logIn(withFirebaseProjectID: projectID, accessToken: accessToken, successBlock: { response, tUser in + guard let password = QBSession.current.sessionDetails?.token else { + completion(nil, response.error?.error) + return + } + tUser.password = password + completion(User(tUser), nil) + }, errorBlock: { response in + completion(nil, response.error?.error) + }) + } + + func connect(withUserID userId: UInt, completion: @escaping (Bool) -> Void) { + guard let token = QBSession.current.sessionDetails?.token else { + completion(false) + return + } + QBChat.instance.connect(withUserID: userId, password: token) { error in + if error != nil { + completion(false) + return + } + completion(true) + } + } + + func disconnect(_ completion: @escaping (Error?) -> Void) { + QBChat.instance.disconnect() {_ in + QBRequest.logOut(successBlock: { response in + completion(nil) + }) { response in + guard let error = response.error?.error else { + completion(nil) + return + } + completion(error) + } + } + } + + private func updateUser(_ updateUserParameter: QBUpdateUserParameters, completion: @escaping (_ user: User?, _ error: Error?) -> Void) { + QBRequest.updateCurrentUser(updateUserParameter, successBlock: { response, user in + completion(User(user), nil) + }, errorBlock: { response in + completion(nil, response.error?.error) + }) + } + + func updateUser(_ update: UpdateUserParameters, completion: @escaping (_ user: User?, _ error: Error?) -> Void) { + + if let avatar = update.avatar { + var compressionQuality: CGFloat = 1.0 + let maxFileSize: Int = 10 * 1024 * 1024 // 10MB in bytes + var imageData = avatar.jpegData(compressionQuality: compressionQuality) + + while let data = imageData, data.count > maxFileSize && compressionQuality > 0.0 { + compressionQuality -= 0.1 + imageData = avatar.jpegData(compressionQuality: compressionQuality) + } + + guard let imageData else { + return + } + + // Sending attachment. + DispatchQueue.main.async(execute: { + let file = FileContent(data: imageData, name: update.name, mimeType: "image/jpeg", isPublic: true) + + self.upload(file: file, completion: { blob in + let parameters = QBUpdateUserParameters() + if let blobId = blob?.id { + parameters.blobID = blobId + } + parameters.fullName = update.name + self.updateUser(parameters) { user, error in + if let error { + completion(nil, error) + } else { + completion(user, nil) + } + } + }) + }) + } else { + let parameters = QBUpdateUserParameters() + parameters.fullName = update.name + self.updateUser(parameters) { user, error in + if let error { + completion(nil, error) + } else { + completion(user, nil) + } + } + } + } + + private func upload(file content: FileContent, completion: @escaping (_ uploadedBlob: QBCBlob?) -> Void) { + QBRequest.tUploadFile(content.data, + fileName: content.name, + contentType: content.mimeType, + isPublic: content.isPublic) { _, blob in + completion(blob) + } statusBlock: { _,_ in + //TODO: add progress handler + } errorBlock: { response in + completion(nil) + } + } + + func getAvatar(_ blobId: UInt, completion: @escaping (_ avatar: UIImage?, _ error: Error?) -> Void) { + QBRequest.blob(withID: blobId, successBlock: { (response, blob) in + guard let blobUID = blob.uid else {return} + QBRequest.downloadFile(withUID: blobUID, successBlock: { (response, fileData) in + if let image = UIImage(data: fileData) { + completion(image, nil) + } + }, statusBlock: { (request, status) in + + }, errorBlock: { (response) in + completion(nil, response.error?.error) + }) + }, errorBlock: { (response) in + completion(nil, response.error?.error) + }) + } + + func deleteAvatar(_ blobId: UInt, completion: @escaping (_ error: Error?) -> Void) { + QBRequest.deleteBlob(withID: blobId) {(response) in + completion(nil) + } errorBlock: { (response) in + completion(response.error?.error) + } + } + + func configure() { + Quickblox.initWithApplicationId(0, + authKey: "", + authSecret: "", + accountKey: "") + + QBSettings.carbonsEnabled = true + QBSettings.autoReconnectEnabled = true + } + + func currentUser() -> User? { + if let currentUser = QBSession.current.currentUser { + return User(currentUser) + } else { + return nil + } + } +} diff --git a/Q-municate/Q-municate/Model/CountryPhoneCode.swift b/Q-municate/Q-municate/Model/CountryPhoneCode.swift new file mode 100644 index 000000000..e7ad4d887 --- /dev/null +++ b/Q-municate/Q-municate/Model/CountryPhoneCode.swift @@ -0,0 +1,68 @@ +// +// CountryCode.swift +// Q-municate +// +// Created by Injoit on 11.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import Foundation + +struct CountryPhoneCodeConstant { + static let base : UInt32 = 127397 +} + +class CountryPhoneCode: Hashable, Codable, Identifiable { + var id: String { + return code + } + + func hash(into hasher: inout Hasher) { + hasher.combine(code) + } + + static func == (lhs: CountryPhoneCode, rhs: CountryPhoneCode) -> Bool { + if lhs.code == rhs.code { return true} + return false + } + + static var defaultCountryPhoneCode: CountryPhoneCode { + return CountryPhoneCode(name: "United States", + dial_code: "+1", + code: "US") + } + + static func getCodes() -> [CountryPhoneCode] { + var countryPhoneCodes: [CountryPhoneCode] = [] + if let path = Bundle.main.url(forResource: "PhoneCountryCodes", withExtension: "json") { + do { + let data = try Data(contentsOf: path, options: .alwaysMapped) + let codes = try JSONDecoder().decode([CountryPhoneCode].self, from: data) + countryPhoneCodes.append(contentsOf: codes) + + } catch let error { + print("Get Country Codes error: \(error.localizedDescription)") + } + } + return countryPhoneCodes + } + + var name: String + var dial_code: String + var code: String + + init(name: String, dial_code: String, code: String) { + self.name = name + self.dial_code = dial_code + self.code = code + } + + var flag: String { + code + .unicodeScalars + .map({ CountryPhoneCodeConstant.base + $0.value }) + .compactMap(UnicodeScalar.init) + .map(String.init) + .joined() + } +} diff --git a/Q-municate/Q-municate/Model/Errors.swift b/Q-municate/Q-municate/Model/Errors.swift new file mode 100644 index 000000000..b86d0c874 --- /dev/null +++ b/Q-municate/Q-municate/Model/Errors.swift @@ -0,0 +1,38 @@ +// +// Errors.swift +// Q-municate +// +// Created by Injoit on 11.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import Foundation + +enum AuthError: Error { + case noPhoneNumberEntered + case invalidPhoneNumber + case noVerificationID + case unvalidCode + case verifyCodeError + case networkError +} + +extension AuthError: LocalizedError { + public var errorDescription: String? { + switch self { + case .noPhoneNumberEntered: + return "Please enter a phone number" + case .noVerificationID: + return "Error fetching verification id" + case .unvalidCode: + return "Please enter a valid code" + case .verifyCodeError: + return "Code verification error" + case .invalidPhoneNumber: + return "The phone number provided is incorrect. Please enter the right phone number" + case .networkError: + return "No Internet Connection" + } + } +} + diff --git a/Q-municate/Q-municate/Model/User.swift b/Q-municate/Q-municate/Model/User.swift new file mode 100644 index 000000000..9664b20e6 --- /dev/null +++ b/Q-municate/Q-municate/Model/User.swift @@ -0,0 +1,47 @@ +// +// User.swift +// Q-municate +// +// Created by Injoit on 15.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import Foundation +import Quickblox + +public struct User { + public let id: String + + /// Display name of the User. + /// + /// > Note: Returns an empty string by default + public var name: String = "" + public var avatarPath: String = "" + public var lastRequestAt: Date = Date(timeIntervalSince1970: 0) + + public var isCurrent: Bool = false + + public init(id: String, + name: String, + isCurrent: Bool = false, + lastRequestAt: Date = + Date(timeIntervalSince1970: 0)) { + self.id = id + self.name = name + self.isCurrent = isCurrent + self.lastRequestAt = lastRequestAt + } +} + +extension User { + init (_ value: QBUUser) { + id = String(value.id) + name = value.fullName ?? "" + if (value.blobID > 0) { + avatarPath = String(value.blobID) + } + lastRequestAt = value.lastRequestAt ?? + Date(timeIntervalSince1970: 0) + isCurrent = QBSession.current.currentUserID == value.id + } +} diff --git a/Q-municate/Q-municate/Preview Content/Preview Assets.xcassets/Contents.json b/Q-municate/Q-municate/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Q-municate/Q-municate/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Q-municate/Q-municate/Q_municateApp.swift b/Q-municate/Q-municate/Q_municateApp.swift new file mode 100644 index 000000000..f228266d6 --- /dev/null +++ b/Q-municate/Q-municate/Q_municateApp.swift @@ -0,0 +1,33 @@ +// +// Q_municateApp.swift +// Q-municate +// +// Created by Injoit on 11.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import SwiftUI + +@main +struct Q_municateApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + @Environment (\.scenePhase) private var scenePhase + + @StateObject var viewModel: EnterViewModel = EnterViewModel() + + var body: some Scene { + WindowGroup { + EnterView(viewModel: viewModel) + .onChange(of: scenePhase) { newPhase in + switch newPhase { + case .active: + if viewModel.authState != .checkVersion { + viewModel.startAuthFlow() + } + default: + print("default") + } + } + } + } +} diff --git a/Q-municate/Q-municate/Resources/AppDelegate.swift b/Q-municate/Q-municate/Resources/AppDelegate.swift new file mode 100644 index 000000000..f56059997 --- /dev/null +++ b/Q-municate/Q-municate/Resources/AppDelegate.swift @@ -0,0 +1,38 @@ +// +// AppDelegate.swift +// Q-municate +// +// Created by Injoit on 11.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import UIKit +import FirebaseAuth +import Firebase + +class AppDelegate: NSObject, UIApplicationDelegate { + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + FirebaseApp.configure() + let api = API() + api.configure() + return true + } + + func application(_ application: UIApplication , didReceiveRemoteNotification notification: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + if Auth.auth().canHandleNotification(notification) { + completionHandler(UIBackgroundFetchResult.noData); + return + } + + } + + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + Auth.auth().setAPNSToken(deviceToken, + type: currentApplicationZone == .prod ? .prod : .sandbox) + } + + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + + } +} diff --git a/Q-municate/Q-municate/Resources/AppTheme/AppTheme.swift b/Q-municate/Q-municate/Resources/AppTheme/AppTheme.swift new file mode 100644 index 000000000..adf921ce5 --- /dev/null +++ b/Q-municate/Q-municate/Resources/AppTheme/AppTheme.swift @@ -0,0 +1,113 @@ +// +// AppTheme.swift +// UIKitSample +// +// Created by Injoit on 15.04.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import SwiftUI +import QuickBloxUIKit +import QBAIRephrase + +var appThemes: [AppTheme] = [AppTheme(color: QuickBloxUIKit.ThemeColor(), + font: QuickBloxUIKit.ThemeFont(), + image: QuickBloxUIKit.ThemeImage(), + string: QuickBloxUIKit.ThemeString(), + qmunicateString: QmunicateString()) +] + +class AppTheme: ThemeProtocol, ObservableObject { + @Published var color: ThemeColorProtocol + @Published var font: ThemeFontProtocol + @Published var image: ThemeImageProtocol + @Published var string: ThemeStringProtocol + @Published var qmunicateString: QmunicateStringProtocol + + init(color: ThemeColorProtocol, + font: ThemeFontProtocol, + image: ThemeImageProtocol, + string: ThemeStringProtocol, + qmunicateString: QmunicateStringProtocol) { + self.color = color + self.font = font + self.image = image + self.string = string + self.qmunicateString = qmunicateString + } +} + +public protocol QmunicateStringProtocol { + var сountry: String { get set } + var number: String { get set } + var privacy: String { get set } + var terms: String { get set } + var enterPhoneNumber: String { get set } + var verifyPhoneNumber: String { get set } + var getCode: String { get set } + var verify: String { get set } + var resendCode: String { get set } + var enterCode: String { get set } + var createProfile: String { get set } + var finish: String { get set } + var save: String { get set } + var editedName: String { get set } + var settings: String { get set } + var enterYourName: String { get set } + var hintName: String { get set } + var dialogs: String { get set } + var cancel: String { get set } + var logOut: String { get set } + var logOutPrompt: String { get set } + var ok: String { get set } + + var update: String { get set } + var newVersion: String { get set } + var resend: String { get set } + var skip: String { get set } + var poweredByQuickblox: String { get set } + var updateToVersion: String { get set } + + +} + +public class QmunicateString: QmunicateStringProtocol { + + public var сountry: String = String(localized: "qmunicate.auth.сountry") + public var number: String = String(localized: "qmunicate.auth.number") + public var privacy: String = String(localized: "qmunicate.auth.privacy") + public var terms: String = String(localized: "qmunicate.auth.terms") + public var enterPhoneNumber: String = String(localized: "qmunicate.auth.enterPhoneNumber") + public var verifyPhoneNumber: String = String(localized: "qmunicate.auth.verifyPhoneNumber") + public var getCode: String = String(localized: "qmunicate.auth.getCode") + public var verify: String = String(localized: "qmunicate.auth.verify") + public var resendCode: String = String(localized: "qmunicate.auth.resendCode") + public var enterCode: String = String(localized: "qmunicate.auth.enterCode") + public var createProfile: String = String(localized: "qmunicate.settings.createProfile") + public var finish: String = String(localized: "qmunicate.settings.finish") + public var save: String = String(localized: "qmunicate.settings.save") + public var editedName: String = String(localized: "qmunicate.settings.editedName") + public var settings: String = String(localized: "qmunicate.settings.settings") + public var enterYourName: String = String(localized: "qmunicate.settings.enterYourName") + public var hintName: String = String(localized: "qmunicate.settings.hint") + public var dialogs: String = String(localized: "qmunicate.settings.dialogs") + public var cancel: String = String(localized: "qmunicate.settings.cancel") + public var logOut: String = String(localized: "qmunicate.settings.logOut") + public var logOutPrompt: String = String(localized: "qmunicate.settings.logOutPrompt") + public var ok: String = String(localized: "qmunicate.settings.ok") + + public var update: String = String(localized: "qmunicate.update.update") + public var newVersion: String = String(localized: "qmunicate.update.newVersion") + public var resend: String = String(localized: "qmunicate.update.resend") + public var skip: String = String(localized: "qmunicate.update.skip") + public var updateToVersion: String = String(localized: "qmunicate.update.updateToVersion") + public var poweredByQuickblox: String = String(localized: "qmunicate.settings.poweredByQuickblox") + + public init() {} +} + +enum ApplicationZone { + case develop, prod, qa +} + +let currentApplicationZone: ApplicationZone = .prod diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Q-municate/Q-municate/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/Q-municate/Q-municate/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/120 1.png b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/120 1.png new file mode 100644 index 000000000..cab75ec8a Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/120 1.png differ diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/120.png b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 000000000..cab75ec8a Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/152.png b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 000000000..53aee5daa Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/152.png differ diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/167.png b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 000000000..e60a3ca35 Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/167.png differ diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/180.png b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 000000000..7f1e1e8e7 Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/20.png b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 000000000..f85cd56d3 Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/20.png differ diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/29.png b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 000000000..b12e73a59 Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/40 1.png b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/40 1.png new file mode 100644 index 000000000..8ef1f44a2 Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/40 1.png differ diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/40 2.png b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/40 2.png new file mode 100644 index 000000000..8ef1f44a2 Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/40 2.png differ diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/40.png b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 000000000..8ef1f44a2 Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/58 1.png b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/58 1.png new file mode 100644 index 000000000..ca6f70711 Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/58 1.png differ diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/58.png b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 000000000..ca6f70711 Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/60.png b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 000000000..e3c24e6bb Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/76.png b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 000000000..ed27916f0 Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/76.png differ diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/80 1.png b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/80 1.png new file mode 100644 index 000000000..fca66a972 Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/80 1.png differ diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/80.png b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 000000000..fca66a972 Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/87.png b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 000000000..e1919bc25 Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..fc07cee94 --- /dev/null +++ b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,116 @@ +{ + "images" : [ + { + "filename" : "40.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "60.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "58.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "87.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "80.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "120 1.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "180.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "20.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "40 1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "29.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58 1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "40 2.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "80 1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "76.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "152.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "167.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "appstore.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/appstore.png b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/appstore.png new file mode 100644 index 000000000..daa4464af Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/AppIcon.appiconset/appstore.png differ diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/Color/Contents.json b/Q-municate/Q-municate/Resources/Assets.xcassets/Color/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Q-municate/Q-municate/Resources/Assets.xcassets/Color/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/Color/backgroundLaunch.colorset/Contents.json b/Q-municate/Q-municate/Resources/Assets.xcassets/Color/backgroundLaunch.colorset/Contents.json new file mode 100644 index 000000000..36a243ea9 --- /dev/null +++ b/Q-municate/Q-municate/Resources/Assets.xcassets/Color/backgroundLaunch.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFC", + "green" : "0x78", + "red" : "0x39" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFC", + "green" : "0x78", + "red" : "0x39" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/Contents.json b/Q-municate/Q-municate/Resources/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Q-municate/Q-municate/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/dialogBackground.imageset/Contents.json b/Q-municate/Q-municate/Resources/Assets.xcassets/dialogBackground.imageset/Contents.json new file mode 100644 index 000000000..6afbdb4f0 --- /dev/null +++ b/Q-municate/Q-municate/Resources/Assets.xcassets/dialogBackground.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "chat_background.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/dialogBackground.imageset/chat_background.png b/Q-municate/Q-municate/Resources/Assets.xcassets/dialogBackground.imageset/chat_background.png new file mode 100644 index 000000000..7b5df1a80 Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/dialogBackground.imageset/chat_background.png differ diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/qmunicate-logo.imageset/Contents.json b/Q-municate/Q-municate/Resources/Assets.xcassets/qmunicate-logo.imageset/Contents.json new file mode 100644 index 000000000..61712556f --- /dev/null +++ b/Q-municate/Q-municate/Resources/Assets.xcassets/qmunicate-logo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "qmunicate-logo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Q-municate/Q-municate/Resources/Assets.xcassets/qmunicate-logo.imageset/qmunicate-logo.pdf b/Q-municate/Q-municate/Resources/Assets.xcassets/qmunicate-logo.imageset/qmunicate-logo.pdf new file mode 100644 index 000000000..17cec158e Binary files /dev/null and b/Q-municate/Q-municate/Resources/Assets.xcassets/qmunicate-logo.imageset/qmunicate-logo.pdf differ diff --git a/Q-municate/Q-municate/Resources/Localizable.xcstrings b/Q-municate/Q-municate/Resources/Localizable.xcstrings new file mode 100644 index 000000000..664685c05 --- /dev/null +++ b/Q-municate/Q-municate/Resources/Localizable.xcstrings @@ -0,0 +1,541 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "" : { + + }, + "qmunicate.auth.createProfile" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Create profile" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Crear perfil" + } + } + } + }, + "qmunicate.auth.editedName" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edited Name" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre editado" + } + } + } + }, + "qmunicate.auth.enterCode" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter the 6-digit code we sent to" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingrese el código de 6 dígitos que le enviamos" + } + } + } + }, + "qmunicate.auth.enterPhoneNumber" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter phone number" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingrese el número de teléfono" + } + } + } + }, + "qmunicate.auth.finish" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Finish" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Finalizar" + } + } + } + }, + "qmunicate.auth.getCode" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Get code" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obtener código" + } + } + } + }, + "qmunicate.auth.number" : { + "comment" : "Number", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Number" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Número" + } + } + } + }, + "qmunicate.auth.privacy" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacy Policy" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Política de privacidad" + } + } + } + }, + "qmunicate.auth.resendCode" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Resend code" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reenviar código" + } + } + } + }, + "qmunicate.auth.settings" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Settings" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración" + } + } + } + }, + "qmunicate.auth.terms" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terms of Service" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Condiciones de servicio" + } + } + } + }, + "qmunicate.auth.verify" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verify" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verificar" + } + } + } + }, + "qmunicate.auth.verifyPhoneNumber" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verify phone number" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verificar número de teléfono" + } + } + } + }, + "qmunicate.auth.сountry" : { + "comment" : "Country", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Country" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "País" + } + } + } + }, + "qmunicate.settings.cancel" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancelar" + } + } + } + }, + "qmunicate.settings.createProfile" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Create profile" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Crear perfil" + } + } + } + }, + "qmunicate.settings.dialogs" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dialogs" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diálogos" + } + } + } + }, + "qmunicate.settings.editedName" : { + + }, + "qmunicate.settings.enterYourName" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter your name" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduzca su nombre" + } + } + } + }, + "qmunicate.settings.finish" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Finish" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Finalizar" + } + } + } + }, + "qmunicate.settings.hint" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Start with a letter, use a-z, A-Z, hyphens, underscores, and spaces. Length: 3-50 characters." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comience con una letra, use a-z, A-Z, guiones, guiones bajos y espacios. Longitud: 3-50 caracteres." + } + } + } + }, + "qmunicate.settings.logOut" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Log out" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cerrar sesión" + } + } + } + }, + "qmunicate.settings.logOutPrompt" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Do you really want to log out from Q-municate?" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Realmente desea cerrar sesión en Q-municate?" + } + } + } + }, + "qmunicate.settings.ok" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ok" + } + }, + "es" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Ok" + } + } + } + }, + "qmunicate.settings.poweredByQuickblox" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Powered by QuickBlox" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Powered by QuickBlox" + } + } + } + }, + "qmunicate.settings.save" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Save" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guardar" + } + } + } + }, + "qmunicate.settings.settings" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Settings" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajustes" + } + } + } + }, + "qmunicate.update.newVersion" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "New Version Available" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nueva versión disponible" + } + } + } + }, + "qmunicate.update.resend" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Re-send in 00:" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reenviar en 00:" + } + } + } + }, + "qmunicate.update.skip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skip this Version" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Omitir esta version" + } + } + } + }, + "qmunicate.update.update" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update" + } + }, + "es" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Actualizar" + } + } + } + }, + "qmunicate.update.updateToVersion" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "A new version of Q-municate is available, please update to version" + } + }, + "es" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Hay una nueva versión de Q-municate disponible; actualice a la versión" + } + } + } + }, + "Re-send in 00:%@" : { + + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/Q-municate/Q-municate/Resources/PhoneCountryCodes.json b/Q-municate/Q-municate/Resources/PhoneCountryCodes.json new file mode 100644 index 000000000..767618868 --- /dev/null +++ b/Q-municate/Q-municate/Resources/PhoneCountryCodes.json @@ -0,0 +1,244 @@ +[ + { "name": "Afghanistan", "dial_code": "+93", "code": "AF" }, + { "name": "Aland Islands", "dial_code": "+358", "code": "AX" }, + { "name": "Albania", "dial_code": "+355", "code": "AL" }, + { "name": "Algeria", "dial_code": "+213", "code": "DZ" }, + { "name": "AmericanSamoa", "dial_code": "+1684", "code": "AS" }, + { "name": "Andorra", "dial_code": "+376", "code": "AD" }, + { "name": "Angola", "dial_code": "+244", "code": "AO" }, + { "name": "Anguilla", "dial_code": "+1264", "code": "AI" }, + { "name": "Antarctica", "dial_code": "+672", "code": "AQ" }, + { "name": "Antigua and Barbuda", "dial_code": "+1268", "code": "AG" }, + { "name": "Argentina", "dial_code": "+54", "code": "AR" }, + { "name": "Armenia", "dial_code": "+374", "code": "AM" }, + { "name": "Aruba", "dial_code": "+297", "code": "AW" }, + { "name": "Australia", "dial_code": "+61", "code": "AU" }, + { "name": "Austria", "dial_code": "+43", "code": "AT" }, + { "name": "Azerbaijan", "dial_code": "+994", "code": "AZ" }, + { "name": "Bahamas", "dial_code": "+1242", "code": "BS" }, + { "name": "Bahrain", "dial_code": "+973", "code": "BH" }, + { "name": "Bangladesh", "dial_code": "+880", "code": "BD" }, + { "name": "Barbados", "dial_code": "+1246", "code": "BB" }, + { "name": "Belarus", "dial_code": "+375", "code": "BY" }, + { "name": "Belgium", "dial_code": "+32", "code": "BE" }, + { "name": "Belize", "dial_code": "+501", "code": "BZ" }, + { "name": "Benin", "dial_code": "+229", "code": "BJ" }, + { "name": "Bermuda", "dial_code": "+1441", "code": "BM" }, + { "name": "Bhutan", "dial_code": "+975", "code": "BT" }, + { "name": "Bolivia, Plurinational State of bolivia", "dial_code": "+591", "code": "BO" }, + { "name": "Bosnia and Herzegovina", "dial_code": "+387", "code": "BA" }, + { "name": "Botswana", "dial_code": "+267", "code": "BW" }, + { "name": "Brazil", "dial_code": "+55", "code": "BR" }, + { "name": "British Indian Ocean Territory", "dial_code": "+246", "code": "IO" }, + { "name": "Brunei Darussalam", "dial_code": "+673", "code": "BN" }, + { "name": "Bulgaria", "dial_code": "+359", "code": "BG" }, + { "name": "Burkina Faso", "dial_code": "+226", "code": "BF" }, + { "name": "Burundi", "dial_code": "+257", "code": "BI" }, + { "name": "Cambodia", "dial_code": "+855", "code": "KH" }, + { "name": "Cameroon", "dial_code": "+237", "code": "CM" }, + { "name": "Canada", "dial_code": "+1", "code": "CA" }, + { "name": "Cape Verde", "dial_code": "+238", "code": "CV" }, + { "name": "Cayman Islands", "dial_code": "+ 345", "code": "KY" }, + { "name": "Central African Republic", "dial_code": "+236", "code": "CF" }, + { "name": "Chad", "dial_code": "+235", "code": "TD" }, + { "name": "Chile", "dial_code": "+56", "code": "CL" }, + { "name": "China", "dial_code": "+86", "code": "CN" }, + { "name": "Christmas Island", "dial_code": "+61", "code": "CX" }, + { "name": "Cocos (Keeling) Islands", "dial_code": "+61", "code": "CC" }, + { "name": "Colombia", "dial_code": "+57", "code": "CO" }, + { "name": "Comoros", "dial_code": "+269", "code": "KM" }, + { "name": "Congo", "dial_code": "+242", "code": "CG" }, + { "name": "Congo, The Democratic Republic of the Congo", "dial_code": "+243", "code": "CD" }, + { "name": "Cook Islands", "dial_code": "+682", "code": "CK" }, + { "name": "Costa Rica", "dial_code": "+506", "code": "CR" }, + { "name": "Cote d'Ivoire", "dial_code": "+225", "code": "CI" }, + { "name": "Croatia", "dial_code": "+385", "code": "HR" }, + { "name": "Cuba", "dial_code": "+53", "code": "CU" }, + { "name": "Cyprus", "dial_code": "+357", "code": "CY" }, + { "name": "Czech Republic", "dial_code": "+420", "code": "CZ" }, + { "name": "Denmark", "dial_code": "+45", "code": "DK" }, + { "name": "Djibouti", "dial_code": "+253", "code": "DJ" }, + { "name": "Dominica", "dial_code": "+1767", "code": "DM" }, + { "name": "Dominican Republic", "dial_code": "+1849", "code": "DO" }, + { "name": "Ecuador", "dial_code": "+593", "code": "EC" }, + { "name": "Egypt", "dial_code": "+20", "code": "EG" }, + { "name": "El Salvador", "dial_code": "+503", "code": "SV" }, + { "name": "Equatorial Guinea", "dial_code": "+240", "code": "GQ" }, + { "name": "Eritrea", "dial_code": "+291", "code": "ER" }, + { "name": "Estonia", "dial_code": "+372", "code": "EE" }, + { "name": "Ethiopia", "dial_code": "+251", "code": "ET" }, + { "name": "Falkland Islands (Malvinas)", "dial_code": "+500", "code": "FK" }, + { "name": "Faroe Islands", "dial_code": "+298", "code": "FO" }, + { "name": "Fiji", "dial_code": "+679", "code": "FJ" }, + { "name": "Finland", "dial_code": "+358", "code": "FI" }, + { "name": "France", "dial_code": "+33", "code": "FR" }, + { "name": "French Guiana", "dial_code": "+594", "code": "GF" }, + { "name": "French Polynesia", "dial_code": "+689", "code": "PF" }, + { "name": "Gabon", "dial_code": "+241", "code": "GA" }, + { "name": "Gambia", "dial_code": "+220", "code": "GM" }, + { "name": "Georgia", "dial_code": "+995", "code": "GE" }, + { "name": "Germany", "dial_code": "+49", "code": "DE" }, + { "name": "Ghana", "dial_code": "+233", "code": "GH" }, + { "name": "Gibraltar", "dial_code": "+350", "code": "GI" }, + { "name": "Greece", "dial_code": "+30", "code": "GR" }, + { "name": "Greenland", "dial_code": "+299", "code": "GL" }, + { "name": "Grenada", "dial_code": "+1473", "code": "GD" }, + { "name": "Guadeloupe", "dial_code": "+590", "code": "GP" }, + { "name": "Guam", "dial_code": "+1671", "code": "GU" }, + { "name": "Guatemala", "dial_code": "+502", "code": "GT" }, + { "name": "Guernsey", "dial_code": "+44", "code": "GG" }, + { "name": "Guinea", "dial_code": "+224", "code": "GN" }, + { "name": "Guinea-Bissau", "dial_code": "+245", "code": "GW" }, + { "name": "Guyana", "dial_code": "+595", "code": "GY" }, + { "name": "Haiti", "dial_code": "+509", "code": "HT" }, + { "name": "Holy See (Vatican City State)", "dial_code": "+379", "code": "VA" }, + { "name": "Honduras", "dial_code": "+504", "code": "HN" }, + { "name": "Hong Kong", "dial_code": "+852", "code": "HK" }, + { "name": "Hungary", "dial_code": "+36", "code": "HU" }, + { "name": "Iceland", "dial_code": "+354", "code": "IS" }, + { "name": "India", "dial_code": "+91", "code": "IN" }, + { "name": "Indonesia", "dial_code": "+62", "code": "ID" }, + { "name": "Iran, Islamic Republic of Persian Gulf", "dial_code": "+98", "code": "IR" }, + { "name": "Iraq", "dial_code": "+964", "code": "IQ" }, + { "name": "Ireland", "dial_code": "+353", "code": "IE" }, + { "name": "Isle of Man", "dial_code": "+44", "code": "IM" }, + { "name": "Israel", "dial_code": "+972", "code": "IL" }, + { "name": "Italy", "dial_code": "+39", "code": "IT" }, + { "name": "Jamaica", "dial_code": "+1876", "code": "JM" }, + { "name": "Japan", "dial_code": "+81", "code": "JP" }, + { "name": "Jersey", "dial_code": "+44", "code": "JE" }, + { "name": "Jordan", "dial_code": "+962", "code": "JO" }, + { "name": "Kazakhstan", "dial_code": "+77", "code": "KZ" }, + { "name": "Kenya", "dial_code": "+254", "code": "KE" }, + { "name": "Kiribati", "dial_code": "+686", "code": "KI" }, + { "name": "Korea, Democratic People's Republic of Korea", "dial_code": "+850", "code": "KP" }, + { "name": "Korea, Republic of South Korea", "dial_code": "+82", "code": "KR" }, + { "name": "Kuwait", "dial_code": "+965", "code": "KW" }, + { "name": "Kyrgyzstan", "dial_code": "+996", "code": "KG" }, + { "name": "Laos", "dial_code": "+856", "code": "LA" }, + { "name": "Latvia", "dial_code": "+371", "code": "LV" }, + { "name": "Lebanon", "dial_code": "+961", "code": "LB" }, + { "name": "Lesotho", "dial_code": "+266", "code": "LS" }, + { "name": "Liberia", "dial_code": "+231", "code": "LR" }, + { "name": "Libyan Arab Jamahiriya", "dial_code": "+218", "code": "LY" }, + { "name": "Liechtenstein", "dial_code": "+423", "code": "LI" }, + { "name": "Lithuania", "dial_code": "+370", "code": "LT" }, + { "name": "Luxembourg", "dial_code": "+352", "code": "LU" }, + { "name": "Macao", "dial_code": "+853", "code": "MO" }, + { "name": "Macedonia", "dial_code": "+389", "code": "MK" }, + { "name": "Madagascar", "dial_code": "+261", "code": "MG" }, + { "name": "Malawi", "dial_code": "+265", "code": "MW" }, + { "name": "Malaysia", "dial_code": "+60", "code": "MY" }, + { "name": "Maldives", "dial_code": "+960", "code": "MV" }, + { "name": "Mali", "dial_code": "+223", "code": "ML" }, + { "name": "Malta", "dial_code": "+356", "code": "MT" }, + { "name": "Marshall Islands", "dial_code": "+692", "code": "MH" }, + { "name": "Martinique", "dial_code": "+596", "code": "MQ" }, + { "name": "Mauritania", "dial_code": "+222", "code": "MR" }, + { "name": "Mauritius", "dial_code": "+230", "code": "MU" }, + { "name": "Mayotte", "dial_code": "+262", "code": "YT" }, + { "name": "Mexico", "dial_code": "+52", "code": "MX" }, + { "name": "Micronesia, Federated States of Micronesia", "dial_code": "+691", "code": "FM" }, + { "name": "Moldova", "dial_code": "+373", "code": "MD" }, + { "name": "Monaco", "dial_code": "+377", "code": "MC" }, + { "name": "Mongolia", "dial_code": "+976", "code": "MN" }, + { "name": "Montenegro", "dial_code": "+382", "code": "ME" }, + { "name": "Montserrat", "dial_code": "+1664", "code": "MS" }, + { "name": "Morocco", "dial_code": "+212", "code": "MA" }, + { "name": "Mozambique", "dial_code": "+258", "code": "MZ" }, + { "name": "Myanmar", "dial_code": "+95", "code": "MM" }, + { "name": "Namibia", "dial_code": "+264", "code": "NA" }, + { "name": "Nauru", "dial_code": "+674", "code": "NR" }, + { "name": "Nepal", "dial_code": "+977", "code": "NP" }, + { "name": "Netherlands", "dial_code": "+31", "code": "NL" }, + { "name": "Netherlands Antilles", "dial_code": "+599", "code": "NA" }, + { "name": "New Caledonia", "dial_code": "+687", "code": "NC" }, + { "name": "New Zealand", "dial_code": "+64", "code": "NZ" }, + { "name": "Nicaragua", "dial_code": "+505", "code": "NI" }, + { "name": "Niger", "dial_code": "+227", "code": "NE" }, + { "name": "Nigeria", "dial_code": "+234", "code": "NG" }, + { "name": "Niue", "dial_code": "+683", "code": "NU" }, + { "name": "Norfolk Island", "dial_code": "+672", "code": "NF" }, + { "name": "Northern Mariana Islands", "dial_code": "+1670", "code": "MP" }, + { "name": "Norway", "dial_code": "+47", "code": "NO" }, + { "name": "Oman", "dial_code": "+968", "code": "OM" }, + { "name": "Pakistan", "dial_code": "+92", "code": "PK" }, + { "name": "Palau", "dial_code": "+680", "code": "PW" }, + { "name": "Palestinian Territory, Occupied", "dial_code": "+970", "code": "PS" }, + { "name": "Panama", "dial_code": "+507", "code": "PA" }, + { "name": "Papua New Guinea", "dial_code": "+675", "code": "PG" }, + { "name": "Paraguay", "dial_code": "+595", "code": "PY" }, + { "name": "Peru", "dial_code": "+51", "code": "PE" }, + { "name": "Philippines", "dial_code": "+63", "code": "PH" }, + { "name": "Pitcairn", "dial_code": "+872", "code": "PN" }, + { "name": "Poland", "dial_code": "+48", "code": "PL" }, + { "name": "Portugal", "dial_code": "+351", "code": "PT" }, + { "name": "Puerto Rico", "dial_code": "+1939", "code": "PR" }, + { "name": "Qatar", "dial_code": "+974", "code": "QA" }, + { "name": "Romania", "dial_code": "+40", "code": "RO" }, + { "name": "Russia", "dial_code": "+7", "code": "RU" }, + { "name": "Rwanda", "dial_code": "+250", "code": "RW" }, + { "name": "Reunion", "dial_code": "+262", "code": "RE" }, + { "name": "Saint Barthelemy", "dial_code": "+590", "code": "BL" }, + { "name": "Saint Helena, Ascension and Tristan Da Cunha", "dial_code": "+290", "code": "SH" }, + { "name": "Saint Kitts and Nevis", "dial_code": "+1869", "code": "KN" }, + { "name": "Saint Lucia", "dial_code": "+1758", "code": "LC" }, + { "name": "Saint Martin", "dial_code": "+590", "code": "MF" }, + { "name": "Saint Pierre and Miquelon", "dial_code": "+508", "code": "PM" }, + { "name": "Saint Vincent and the Grenadines", "dial_code": "+1784", "code": "VC" }, + { "name": "Samoa", "dial_code": "+685", "code": "WS" }, + { "name": "San Marino", "dial_code": "+378", "code": "SM" }, + { "name": "Sao Tome and Principe", "dial_code": "+239", "code": "ST" }, + { "name": "Saudi Arabia", "dial_code": "+966", "code": "SA" }, + { "name": "Senegal", "dial_code": "+221", "code": "SN" }, + { "name": "Serbia", "dial_code": "+381", "code": "RS" }, + { "name": "Seychelles", "dial_code": "+248", "code": "SC" }, + { "name": "Sierra Leone", "dial_code": "+232", "code": "SL" }, + { "name": "Singapore", "dial_code": "+65", "code": "SG" }, + { "name": "Slovakia", "dial_code": "+421", "code": "SK" }, + { "name": "Slovenia", "dial_code": "+386", "code": "SI" }, + { "name": "Solomon Islands", "dial_code": "+677", "code": "SB" }, + { "name": "Somalia", "dial_code": "+252", "code": "SO" }, + { "name": "South Africa", "dial_code": "+27", "code": "ZA" }, + { "name": "South Sudan", "dial_code": "+211", "code": "SS" }, + { "name": "South Georgia and the South Sandwich Islands", "dial_code": "+500", "code": "GS" }, + { "name": "Spain", "dial_code": "+34", "code": "ES" }, + { "name": "Sri Lanka", "dial_code": "+94", "code": "LK" }, + { "name": "Sudan", "dial_code": "+249", "code": "SD" }, + { "name": "Suriname", "dial_code": "+597", "code": "SR" }, + { "name": "Svalbard and Jan Mayen", "dial_code": "+47", "code": "SJ" }, + { "name": "Swaziland", "dial_code": "+268", "code": "SZ" }, + { "name": "Sweden", "dial_code": "+46", "code": "SE" }, + { "name": "Switzerland", "dial_code": "+41", "code": "CH" }, + { "name": "Syrian Arab Republic", "dial_code": "+963", "code": "SY" }, + { "name": "Taiwan", "dial_code": "+886", "code": "TW" }, + { "name": "Tajikistan", "dial_code": "+992", "code": "TJ" }, + { "name": "Tanzania, United Republic of Tanzania", "dial_code": "+255", "code": "TZ" }, + { "name": "Thailand", "dial_code": "+66", "code": "TH" }, + { "name": "Timor-Leste", "dial_code": "+670", "code": "TL" }, + { "name": "Togo", "dial_code": "+228", "code": "TG" }, + { "name": "Tokelau", "dial_code": "+690", "code": "TK" }, + { "name": "Tonga", "dial_code": "+676", "code": "TO" }, + { "name": "Trinidad and Tobago", "dial_code": "+1868", "code": "TT" }, + { "name": "Tunisia", "dial_code": "+216", "code": "TN" }, + { "name": "Turkey", "dial_code": "+90", "code": "TR" }, + { "name": "Turkmenistan", "dial_code": "+993", "code": "TM" }, + { "name": "Turks and Caicos Islands", "dial_code": "+1649", "code": "TC" }, + { "name": "Tuvalu", "dial_code": "+688", "code": "TV" }, + { "name": "Uganda", "dial_code": "+256", "code": "UG" }, + { "name": "Ukraine", "dial_code": "+380", "code": "UA" }, + { "name": "United Arab Emirates", "dial_code": "+971", "code": "AE" }, + { "name": "United Kingdom", "dial_code": "+44", "code": "GB" }, + { "name": "United States", "dial_code": "+1", "code": "US" }, + { "name": "Uruguay", "dial_code": "+598", "code": "UY" }, + { "name": "Uzbekistan", "dial_code": "+998", "code": "UZ" }, + { "name": "Vanuatu", "dial_code": "+678", "code": "VU" }, + { "name": "Venezuela, Bolivarian Republic of Venezuela", "dial_code": "+58", "code": "VE" }, + { "name": "Vietnam", "dial_code": "+84", "code": "VN" }, + { "name": "Virgin Islands, British", "dial_code": "+1284", "code": "VG" }, + { "name": "Virgin Islands, U.S.", "dial_code": "+1340", "code": "VI" }, + { "name": "Wallis and Futuna", "dial_code": "+681", "code": "WF" }, + { "name": "Yemen", "dial_code": "+967", "code": "YE" }, + { "name": "Zambia", "dial_code": "+260", "code": "ZM" }, + { "name": "Zimbabwe", "dial_code": "+263", "code": "ZW" } +] diff --git a/Q-municate/Q-municate/Resources/Settings/CountryScreenSettings.swift b/Q-municate/Q-municate/Resources/Settings/CountryScreenSettings.swift new file mode 100644 index 000000000..6297c26dd --- /dev/null +++ b/Q-municate/Q-municate/Resources/Settings/CountryScreenSettings.swift @@ -0,0 +1,110 @@ +// +// CountryScreenSettings.swift +// Q-municate +// +// Created by Injoit on 11.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import SwiftUI +import QuickBloxUIKit + +public class CountryScreenSettings { + public var header: CountryHeaderSettings + public var searchBar: CountrySearchBarSettings + public var backgroundColor: Color + public var countryRow: CountryRowSettings + public var blurRadius: CGFloat = 12.0 + public var progressBar: ProgressBarSettings + + init(_ theme: AppTheme) { + self.header = CountryHeaderSettings(theme) + self.searchBar = CountrySearchBarSettings(theme) + self.backgroundColor = theme.color.mainBackground + self.countryRow = CountryRowSettings(theme) + self.progressBar = ProgressBarSettings(theme) + } +} + +public struct CountrySearchBarSettings { + public var isSearchable: Bool = true + public var searchTextField: DialogsSearchTextField + + init(_ theme: AppTheme) { + self.searchTextField = DialogsSearchTextField(theme) + } + + public struct DialogsSearchTextField { + public var placeholderText: String + public var placeholderColor: Color + public var backgroundColor: Color + + init(_ theme: AppTheme) { + self.placeholderColor = theme.color.secondaryText + self.backgroundColor = theme.color.inputBackground + self.placeholderText = theme.string.search + } + } +} + +public struct CountryRowSettings { + public var backgroundColor: Color + public var iconFont: Font + public var codeColor: Color + public var codeFont: Font + public var selectHeight: CGFloat = 56 + public var selectPadding: EdgeInsets = EdgeInsets(top: 0, + leading: 16, + bottom: 0, + trailing: 16) + + init(_ theme: AppTheme) { + self.backgroundColor = theme.color.mainBackground + self.iconFont = theme.font.callout + self.codeFont = theme.font.callout + self.codeColor = theme.color.mainText + } +} + +public struct CountryHeaderSettings { + public var displayMode: NavigationBarItem.TitleDisplayMode = .inline + public var backgroundColor: Color + public var title: CountryTitle + public var leftButton: BackButton + public var isHidden: Bool = false + + init(_ theme: AppTheme) { + self.backgroundColor = theme.color.mainBackground + self.title = CountryTitle(theme) + self.leftButton = BackButton(theme) + } + + public struct BackButton { + public var title: String? + public var image: Image + public var color: Color + public var scale: Double = 0.5 + public var padding: EdgeInsets = EdgeInsets(top: 0.0, + leading: 16.0, + bottom: 0.0, + trailing: 0.0) + + init(_ theme: AppTheme) { + self.image = theme.image.back + self.color = theme.color.mainElements + } + } + + public struct CountryTitle { + public var text: String + public var color: Color + public var font: Font + + init(_ theme: AppTheme) { + self.font = theme.font.headline + self.color = theme.color.mainText + self.text = theme.qmunicateString.сountry + } + } +} + diff --git a/Q-municate/Q-municate/Resources/Settings/EnterPhoneNumberSettings.swift b/Q-municate/Q-municate/Resources/Settings/EnterPhoneNumberSettings.swift new file mode 100644 index 000000000..d2050e863 --- /dev/null +++ b/Q-municate/Q-municate/Resources/Settings/EnterPhoneNumberSettings.swift @@ -0,0 +1,136 @@ +// +// EnterPhoneNumberSettings.swift +// Q-municate +// +// Created by Injoit on 11.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import SwiftUI +import QuickBloxUIKit + +public class EnterPhoneNumberScreenSettings { + public var header: EnterPhoneNumberHeaderSettings + public var countryScreen: CountryScreenSettings + public var verifyCodeScreen: VerifyPhoneNumberScreenSettings + public var backgroundColor: Color + public var countryCode: CountryCodeSettings + public var phoneNumber: PhoneNumberSettings + public var terms: TermsSettings + public var blurRadius: CGFloat = 12.0 + public var progressBar: ProgressBarSettings + public var titleOk: String + + init(_ theme: AppTheme) { + self.header = EnterPhoneNumberHeaderSettings(theme) + self.countryScreen = CountryScreenSettings(theme) + self.verifyCodeScreen = VerifyPhoneNumberScreenSettings(theme) + self.backgroundColor = theme.color.mainBackground + self.countryCode = CountryCodeSettings(theme) + self.phoneNumber = PhoneNumberSettings(theme) + self.terms = TermsSettings(theme) + self.progressBar = ProgressBarSettings(theme) + self.titleOk = theme.qmunicateString.ok + } +} + +public struct CountryCodeSettings { + public var title: String + public var titleColor: Color + public var titleFont: Font + public var iconFont: Font + public var codeColor: Color + public var codeFont: Font + public var arrowRight: Image + public var arrowColor: Color + public var tralingSpacing: CGFloat = 12 + + init(_ theme: AppTheme) { + self.titleFont = theme.font.headline + self.titleColor = theme.color.mainText + self.title = theme.qmunicateString.сountry + self.iconFont = theme.font.callout + self.codeFont = theme.font.callout + self.codeColor = theme.color.mainElements + self.arrowRight = theme.image.chevronForward + self.arrowColor = theme.color.mainText + } +} + +public struct PhoneNumberSettings { + public var title: String + public var titleColor: Color + public var titleFont: Font + public var numberColor: Color + public var numberFont: Font + + init(_ theme: AppTheme) { + self.titleFont = theme.font.headline + self.titleColor = theme.color.mainText + self.title = theme.qmunicateString.number + self.numberFont = theme.font.callout + self.numberColor = theme.color.mainText + } +} + +public struct TermsHeaderSettings { + public var displayMode: NavigationBarItem.TitleDisplayMode = .inline + public var backgroundColor: Color + public var isHidden: Bool = false + + init(_ theme: AppTheme) { + self.backgroundColor = theme.color.mainBackground + } +} + +public struct TermsSettings { + public var privacyPolicy: String + public var terms: String + public var color: Color + public var font: Font + public var header: TermsHeaderSettings + + init(_ theme: AppTheme) { + self.font = theme.font.caption + self.color = theme.color.secondaryText + self.privacyPolicy = theme.qmunicateString.privacy + self.terms = theme.qmunicateString.terms + self.header = TermsHeaderSettings(theme) + } +} + +public struct EnterPhoneNumberHeaderSettings { + public var displayMode: NavigationBarItem.TitleDisplayMode = .inline + public var backgroundColor: Color + public var title: EnterPhoneNumberTitle + public var rightButton: GetCodeButton + public var isHidden: Bool = false + + init(_ theme: AppTheme) { + self.backgroundColor = theme.color.mainBackground + self.title = EnterPhoneNumberTitle(theme) + self.rightButton = GetCodeButton(theme) + } + + public struct GetCodeButton { + public var title: String + public var color: Color + + init(_ theme: AppTheme) { + self.title = theme.qmunicateString.getCode + self.color = theme.color.mainElements + } + } + + public struct EnterPhoneNumberTitle { + public var text: String + public var color: Color + public var font: Font + + init(_ theme: AppTheme) { + self.font = theme.font.headline + self.color = theme.color.mainText + self.text = theme.qmunicateString.enterPhoneNumber + } + } +} diff --git a/Q-municate/Q-municate/Resources/Settings/EnterScreenSettings.swift b/Q-municate/Q-municate/Resources/Settings/EnterScreenSettings.swift new file mode 100644 index 000000000..e9c7216f9 --- /dev/null +++ b/Q-municate/Q-municate/Resources/Settings/EnterScreenSettings.swift @@ -0,0 +1,37 @@ +// +// EnterScreenSettings.swift +// Q-municate +// +// Created by Injoit on 11.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import SwiftUI +import QuickBloxUIKit + +public class EnterScreenSettings { + public var updateVersionAlert: UpdateVersionAlertSettings + public var poweredByQuickblox: String + + init(_ theme: AppTheme) { + self.updateVersionAlert = UpdateVersionAlertSettings(theme) + self.poweredByQuickblox = theme.qmunicateString.poweredByQuickblox + } +} + +public struct UpdateVersionAlertSettings { + public var update: String + public var newVersion: String + public var resend: String + public var skip: String + + public var updateToVersion: String + + init(_ theme: AppTheme) { + self.update = theme.qmunicateString.update + self.newVersion = theme.qmunicateString.newVersion + self.resend = theme.qmunicateString.resend + self.skip = theme.qmunicateString.skip + self.updateToVersion = theme.qmunicateString.updateToVersion + } +} diff --git a/Q-municate/Q-municate/Resources/Settings/ProfileScreenSettings.swift b/Q-municate/Q-municate/Resources/Settings/ProfileScreenSettings.swift new file mode 100644 index 000000000..6c2a2e166 --- /dev/null +++ b/Q-municate/Q-municate/Resources/Settings/ProfileScreenSettings.swift @@ -0,0 +1,140 @@ +// +// ProfileScreenSettings.swift +// Q-municate +// +// Created by Injoit on 14.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import SwiftUI +import UniformTypeIdentifiers + +public class ProfileScreenSettings { + public var header: SettingsHeaderSettings + public var backgroundColor: Color + public var avatar: Image + public var blurRadius: CGFloat = 12.0 + public var dividerColor: Color + public var height: CGFloat = 56.0 + public var spacing: CGFloat = 16.0 + public var hint: HintSettings + public var textfieldPrompt: String + public var mediaAlert: MediaAlert + public var avatarSize: CGSize = CGSize(width: 80.0, height: 80.0) + public var isHiddenFiles: Bool = true + public var maximumMB: Double = 10 + public var logoutButton: LogoutButton + public var logoutAlert: LogoutAlertSettings + + init(_ theme: AppTheme) { + self.header = SettingsHeaderSettings(theme) + self.mediaAlert = MediaAlert(theme) + self.backgroundColor = theme.color.mainBackground + self.avatar = theme.image.avatarUser + self.dividerColor = theme.color.divider + self.hint = HintSettings(theme) + self.textfieldPrompt = theme.qmunicateString.enterYourName + self.logoutButton = LogoutButton(theme) + self.logoutAlert = LogoutAlertSettings(theme) + } +} + +public struct LogoutAlertSettings { + public var title: String + public var cancel: String + public var ok: String + + init(_ theme: AppTheme) { + self.title = theme.qmunicateString.logOutPrompt + self.cancel = theme.qmunicateString.cancel + self.ok = theme.qmunicateString.ok + } +} + +public struct LogoutButton { + public var title: String + public var color: Color + public var font: Font + public var size: CGSize = CGSize(width: 128, height: 32) + + init(_ theme: AppTheme) { + self.color = theme.color.mainElements + self.title = theme.qmunicateString.logOut + self.font = theme.font.headline + } +} + +public struct HintSettings { + public var text: String + public var color: Color + public var font: Font + + init(_ theme: AppTheme) { + self.font = theme.font.caption + self.color = theme.color.secondaryElements.opacity(0.4) + self.text = theme.qmunicateString.hintName + } +} + +public struct MediaAlert { + public var title: String + public var removePhoto: String + public var camera: String + public var gallery: String + public var cancel: String + public var file: String + public var galleryMediaTypes: [String] = [UTType.image.identifier] + public var fileMediaTypes: [UTType] = [.jpeg, .png, .heic, .heif, .image] + public var blurRadius:CGFloat = 12.0 + + init(_ theme: AppTheme) { + self.title = theme.string.photo + self.removePhoto = theme.string.removePhoto + self.camera = theme.string.camera + self.gallery = theme.string.gallery + self.file = theme.string.file + self.cancel = theme.string.cancel + } +} + +public struct SettingsHeaderSettings { + public var title: ProfileTitle + public var rightButton: FinishButton + + public var displayMode: NavigationBarItem.TitleDisplayMode = .inline + public var backgroundColor: Color + public var opacity: CGFloat = 0.4 + public var isHidden: Bool = false + + init(_ theme: AppTheme) { + self.backgroundColor = theme.color.mainBackground + self.title = ProfileTitle(theme) + self.rightButton = FinishButton(theme) + } + + public struct FinishButton { + public var finish: String + public var save: String + public var color: Color + + init(_ theme: AppTheme) { + self.color = theme.color.mainElements + self.finish = theme.qmunicateString.finish + self.save = theme.qmunicateString.save + } + } + + public struct ProfileTitle { + public var settings: String + public var profile: String + public var color: Color + public var font: Font + + init(_ theme: AppTheme) { + self.font = theme.font.headline + self.color = theme.color.mainText + self.settings = theme.qmunicateString.settings + self.profile = theme.qmunicateString.createProfile + } + } +} diff --git a/Q-municate/Q-municate/Resources/Settings/VerifyPhoneNumberScreenSettings.swift b/Q-municate/Q-municate/Resources/Settings/VerifyPhoneNumberScreenSettings.swift new file mode 100644 index 000000000..e7230701a --- /dev/null +++ b/Q-municate/Q-municate/Resources/Settings/VerifyPhoneNumberScreenSettings.swift @@ -0,0 +1,124 @@ +// +// VerifyPhoneNumberScreenSettings.swift +// Q-municate +// +// Created by Injoit on 14.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import SwiftUI +import QuickBloxUIKit + +public class VerifyPhoneNumberScreenSettings { + public var header: VerifyPhoneNumberHeaderSettings + public var enterCodeLabel: EnterCodeLabelSettings + public var codeFild: CodeFildSettings + public var backgroundColor: Color + public var resendCode: ResendCodeButton + public var blurRadius: CGFloat = 12.0 + public var progressBar: ProgressBarSettings + + init(_ theme: AppTheme) { + self.header = VerifyPhoneNumberHeaderSettings(theme) + self.enterCodeLabel = EnterCodeLabelSettings(theme) + self.codeFild = CodeFildSettings(theme) + self.backgroundColor = theme.color.mainBackground + self.resendCode = ResendCodeButton(theme) + self.progressBar = ProgressBarSettings(theme) + } +} + +public struct ResendCodeButton { + public var title: String + public var color: Color + public var font: Font + public var size: CGSize = CGSize(width: 128, height: 32) + + init(_ theme: AppTheme) { + self.title = theme.qmunicateString.resendCode + self.color = theme.color.mainElements + self.font = theme.font.headline + } +} + +public struct CodeFildSettings { + public var digitColor: Color + public var digitFont: Font + public var size: CGSize = CGSize(width: 26, height: 48) + + init(_ theme: AppTheme) { + self.digitColor = theme.color.mainText + self.digitFont = theme.font.largeTitle + } +} + +public struct EnterCodeLabelSettings { + public var title: String + public var titleColor: Color + public var titleFont: Font + public var numberColor: Color + public var numberFont: Font + + init(_ theme: AppTheme) { + self.titleFont = theme.font.callout + self.titleColor = theme.color.mainText + self.title = theme.qmunicateString.enterCode + self.numberFont = theme.font.callout + self.numberColor = theme.color.mainElements + } +} + +public struct VerifyPhoneNumberHeaderSettings { + public var displayMode: NavigationBarItem.TitleDisplayMode = .inline + public var backgroundColor: Color + public var title: VerifyPhoneNumberTitle + public var leftButton: BackButton + public var rightButton: VerifyButton + public var isHidden: Bool = false + + init(_ theme: AppTheme) { + self.backgroundColor = theme.color.mainBackground + self.title = VerifyPhoneNumberTitle(theme) + self.leftButton = BackButton(theme) + self.rightButton = VerifyButton(theme) + } + + public struct BackButton { + public var imageSize: CGSize? + public var frame: CGSize? + public var image: Image + public var color: Color + public var scale: Double = 0.6 + public var padding: EdgeInsets = EdgeInsets(top: 0.0, + leading: 0.0, + bottom: 0.0, + trailing: 10.0) + public init(_ theme: ThemeProtocol) { + self.image = theme.image.back + self.color = theme.color.mainElements + } + } + + public struct VerifyButton { + public var title: String + public var color: Color + + init(_ theme: AppTheme) { + self.title = theme.qmunicateString.verify + self.color = theme.color.mainElements + } + } + + public struct VerifyPhoneNumberTitle { + public var text: String + public var color: Color + public var font: Font + + init(_ theme: AppTheme) { + self.font = theme.font.headline + self.color = theme.color.mainText + self.text = theme.qmunicateString.verifyPhoneNumber + } + } +} + diff --git a/Q-municate/Q-municate/Supporting Files/Info.plist b/Q-municate/Q-municate/Supporting Files/Info.plist new file mode 100644 index 000000000..c45c59a23 --- /dev/null +++ b/Q-municate/Q-municate/Supporting Files/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleAllowMixedLocalizations + + CFBundleURLTypes + + + CFBundleURLSchemes + + + + LSApplicationQueriesSchemes + + fbapi + fb-messenger-api + fbauth2 + fbshareextension + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + BGTaskSchedulerPermittedIdentifiers + + $(PRODUCT_BUNDLE_IDENTIFIER) + + UIBackgroundModes + + fetch + processing + remote-notification + voip + + + diff --git a/Q-municate/Q-municate/Supporting Files/Q-municate.entitlements b/Q-municate/Q-municate/Supporting Files/Q-municate.entitlements new file mode 100644 index 000000000..aa69e58bd --- /dev/null +++ b/Q-municate/Q-municate/Supporting Files/Q-municate.entitlements @@ -0,0 +1,12 @@ + + + + + aps-environment + development + keychain-access-groups + + $(AppIdentifierPrefix)com.quickblox.qmunicate + + + diff --git a/Q-municate/Q-municate/View/CountryView.swift b/Q-municate/Q-municate/View/CountryView.swift new file mode 100644 index 000000000..aca59bec3 --- /dev/null +++ b/Q-municate/Q-municate/View/CountryView.swift @@ -0,0 +1,50 @@ +// +// CountryView.swift +// Q-municate +// +// Created by Injoit on 12.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import SwiftUI + +struct CountryView: View { + private var settings: CountryScreenSettings + + @ObservedObject private var viewModel: EnterViewModel + @State private var isForwardFailedPresented: Bool = false + @State var isPresented: Bool = false + + init(viewModel: EnterViewModel, + settings: CountryScreenSettings) { + self.viewModel = viewModel + self.settings = settings + } + + public var body: some View { + ZStack { + VStack(spacing: 0) { + SelectCountryListView(viewModel: viewModel, settings: settings) + } + } + .modifier(CountryHeader(settings: settings.header)) + } +} + +public struct CountryHeader: ViewModifier { + private var settings: CountryHeaderSettings + + public init(settings: CountryHeaderSettings) { + self.settings = settings + } + + public func body(content: Content) -> some View { + content + .navigationTitle(settings.title.text) + .navigationBarTitleDisplayMode(settings.displayMode) + .navigationBarBackButtonHidden(false) + .navigationBarHidden(settings.isHidden) + .toolbarBackground(settings.backgroundColor,for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) + } +} diff --git a/Q-municate/Q-municate/View/CreateProfileView.swift b/Q-municate/Q-municate/View/CreateProfileView.swift new file mode 100644 index 000000000..be12a06af --- /dev/null +++ b/Q-municate/Q-municate/View/CreateProfileView.swift @@ -0,0 +1,773 @@ +// +// CreateProfileView.swift +// Q-municate +// +// Created by Injoit on 14.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import SwiftUI +import QuickBloxDomain +import PhotosUI +import QuickBloxData +import QuickBloxUIKit + +struct CreateProfileView: View { + @Environment(\.dismiss) var dismiss + + private var settings: ProfileScreenSettings + + @ObservedObject private var viewModel: EnterViewModel + + @State private var isAlertPresented: Bool = false + @State private var presentCreateDialog: Bool = false + @State private var isSizeAlertPresented: Bool = false + @State private var isLogOutAlertPresented: Bool = false + + @FocusState private var isFocused: Bool + + @State private var attachmentAsset: AttachmentAsset? = nil + + init(_ viewModel: EnterViewModel, + theme: AppTheme) { + self.viewModel = viewModel + self.settings = ProfileScreenSettings(theme) + } + + + public var body: some View { + container() + } + + @ViewBuilder + private func container() -> some View { + NavigationStack { + ZStack { + settings.backgroundColor.ignoresSafeArea() + VStack(spacing: 0) { + HStack(spacing: settings.spacing) { + ProfilePhoto(selectedImage: $viewModel.avatar, + isLoadingAvatar: viewModel.isLoadingAvatar, + onTap: { + isAlertPresented = true + }, settings: settings) + + ProfileNameTextField(profileName: $viewModel.userName, + isValidProfileName: viewModel.isValidUserName, + settings: settings) + .focused($isFocused) + }.padding([.leading, .trailing]) + .padding(.top) + .frame(maxHeight: 140) + + if viewModel.user?.name.isEmpty == false { + Button { + isLogOutAlertPresented = true + } label: { + Text(settings.logoutButton.title) + .foregroundColor(settings.logoutButton.color) + .font(settings.logoutButton.font) + }.frame(width: settings.logoutButton.size.width, + height: settings.logoutButton.size.height) + .padding(.top, 44) + } + + Spacer() + } + } + + .if(isLogOutAlertPresented == true, transform: { view in + view.logoutAlert(isPresented: $isLogOutAlertPresented, onSubmit: { + viewModel.logOut() + }, settings: settings) + }) + + .modifier(ProfileHeader(onTap: { + isFocused = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.viewModel.updateUser() + } + }, settings: settings.header, + isSave: viewModel.user?.name.isEmpty == false, + disabled: viewModel.isValidUserName == false)) + + .mediaAlert(isAlertPresented: $isAlertPresented, + isExistingImage: viewModel.isExistingImage, + mediaTypes: [.images], + viewModel: viewModel, + onRemoveImage: { + viewModel.removeExistingImage() + }, onGetAvatarImage: { avatarImage in + viewModel.handleOnSelect(avatarImage) + + }) + + .disabled(viewModel.isProcessing == true) + .if(viewModel.isProcessing == true) { view in + view.overlay() { + CustomProgressView() + } + } + .onAppear() { + isFocused = false + viewModel.setupUserInfo() + } + .onDisappear { + isFocused = false + viewModel.resetUserInfo() + } + + } + } +} + +public struct ProfilePhoto: View { + private var settings: ProfileScreenSettings + + @Binding var selectedImage: UIImage? + let onTap: () -> Void + + var avatarImage: Image? { + if let selectedImage { + return Image(uiImage: selectedImage) + } + return nil + } + + var isLoadingAvatar: Bool + + init(selectedImage: Binding, + isLoadingAvatar: Bool, + onTap: @escaping () -> Void, + settings: ProfileScreenSettings) { + _selectedImage = selectedImage + self.isLoadingAvatar = isLoadingAvatar + self.onTap = onTap + self.settings = settings + } + + public var body: some View { + VStack { + ZStack { + if let avatarImage { + avatarImage + .avatarModifier(height: settings.height) + .onTapGesture { + onTap() + } + } else if isLoadingAvatar == true { + settings.avatar + .avatarModifier(height: settings.height).opacity(0.6) + .overlay { + ProgressView().tint(.white) + } + } else { + settings.avatar + .avatarModifier(height: settings.height) + .onTapGesture { + onTap() + } + } + } + Spacer() + } + } +} + +extension Image { + func avatarModifier(height: CGFloat) -> some View { + self + .resizable() + .scaledToFill() + .frame(width: height, height: height) + .clipShape(Circle()) + } +} + +extension Color { + func avatarModifier(height: CGFloat) -> some View { + self + .frame(width: height, height: height) + .clipShape(Circle()) + } +} + +public struct ProfileNameTextField: View { + private var settings: ProfileScreenSettings + + @Binding var profileName: String + var isValidProfileName: Bool = false + @FocusState private var isFocused: Bool + + init(profileName: Binding, + isValidProfileName: Bool, + settings: ProfileScreenSettings) { + _profileName = profileName + self.isValidProfileName = isValidProfileName + self.settings = settings + } + + public var body: some View { + VStack(spacing: settings.spacing / 2) { + TextField(settings.textfieldPrompt, text: $profileName, onEditingChanged: { (changed) in + isFocused = changed + }).padding(.top) + + Divider() + .background(settings.hint.color) + + Text(settings.hint.text) + .font(settings.hint.font) + .foregroundColor(settings.hint.color) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + } + } +} + +struct LogoutAlert: ViewModifier { + private var settings: ProfileScreenSettings + @Binding var isPresented: Bool + let onSubmit: () -> Void + + init(settings: ProfileScreenSettings, + isPresented: Binding, + onSubmit: @escaping () -> Void) { + self.settings = settings + _isPresented = isPresented + self.onSubmit = onSubmit + } + + func body(content: Content) -> some View { + ZStack(alignment: .center) { + content.blur(radius: isPresented ? 12.0 : 0.0) + .disabled(isPresented) + .alert(settings.logoutAlert.title, isPresented: $isPresented) { + Button(settings.logoutAlert.cancel, role: .cancel, action: { + isPresented = false + }) + Button(settings.logoutAlert.ok, action: { + isPresented = false + onSubmit() + }) + } message: { + Text("") + } + } + } +} + +extension View { + func logoutAlert( + isPresented: Binding, + onSubmit: @escaping () -> Void, + settings: ProfileScreenSettings + ) -> some View { + self.modifier(LogoutAlert(settings: settings, isPresented: isPresented, onSubmit: onSubmit)) + } +} + +struct ProfileHeaderToolbarContent: ToolbarContent { + + private var settings: SettingsHeaderSettings + private var isSave: Bool + private var disabled: Bool + + let onTap: () -> Void + + init(onTap: @escaping () -> Void, + settings: SettingsHeaderSettings, + isSave: Bool, + disabled: Bool) { + self.onTap = onTap + self.settings = settings + self.isSave = isSave + self.disabled = disabled + } + + public var body: some ToolbarContent { + ToolbarItem(placement: .principal) { + Text(isSave == true ? settings.title.settings : settings.title.profile) + .font(settings.title.font) + .foregroundColor(settings.title.color) + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button { + onTap() + } label: { + Text(isSave == true ? settings.rightButton.save : settings.rightButton.finish) + .foregroundColor(disabled == false ? settings.rightButton.color : settings.rightButton.color.opacity(0.4)) + }.disabled(disabled) + } + } +} + +public struct ProfileHeader: ViewModifier { + + private var settings: SettingsHeaderSettings + private var isSave: Bool + private var disabled: Bool + + let onTap: () -> Void + + init(onTap: @escaping () -> Void, + settings: SettingsHeaderSettings, + isSave: Bool, + disabled: Bool) { + self.onTap = onTap + self.settings = settings + self.isSave = isSave + self.disabled = disabled + } + + public func body(content: Content) -> some View { + content.toolbar { + ProfileHeaderToolbarContent(onTap: onTap, settings: settings, isSave: isSave, disabled: disabled) + } + .navigationTitle("") + .navigationBarTitleDisplayMode(settings.displayMode) + .navigationBarBackButtonHidden(true) + .navigationBarHidden(settings.isHidden) + .toolbarBackground(settings.backgroundColor,for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) + .toolbarRole(.editor) + } +} + +public struct CustomMediaAlert: ViewModifier { + public var settings = QuickBloxUIKit.settings.dialogNameScreen.mediaAlert + + @ObservedObject var viewModel: EnterViewModel + + @Binding var isAlertPresented: Bool + @State var isImagePickerPresented: Bool = false + @State var isCameraPresented: Bool = false + @State var selectedItem: PhotosPickerItem? = nil + @State private var avatarImage: UIImage? { + didSet { + if let avatarImage { + onGetAvatarImage(avatarImage) + defaultState() + } + } + } + + var mediaPickerActions: [MediaPickerAction] { + var mediaPickerAction: [MediaPickerAction] = [.camera, .photo] + if isExistingImage == true { + mediaPickerAction = [.removePhoto, .camera, .photo] + } + return mediaPickerAction + } + + var isExistingImage: Bool + let mediaTypes: [PHPickerFilter] + + let onRemoveImage: () -> Void + let onGetAvatarImage: (_ image: UIImage) -> Void + + public func body(content: Content) -> some View { + ZStack { + content + .blur(radius: isAlertPresented || isImagePickerPresented ? settings.blurRadius : 0.0) + + .if(isIphone == true, transform: { view in + view.confirmationDialog(settings.title, isPresented: $isAlertPresented, actions: { + if isExistingImage == true { + Button(settings.removePhoto, role: .destructive) { + onRemoveImage() + defaultState() + } + } + Button(settings.camera, role: .none) { + isCameraPresented = true + } + Button(settings.gallery, role: .none, action: { + isImagePickerPresented = true + }) + Button(settings.cancel, role: .cancel) { + defaultState() + } + }) + }) + + .if(isIPad == true && isAlertPresented == true, transform: { view in + ZStack { + view.disabled(true) + .overlay( + VStack(spacing: 8) { + VStack { + ForEach(mediaPickerActions, id:\.self) { action in + MediaPickerSegmentView(action: action) { action in + switch action { + case .removePhoto: + onRemoveImage() + defaultState() + case .camera: + isCameraPresented = true + case .photo: + isImagePickerPresented = true + } + } + + if mediaPickerActions.last != action { + Divider() + } + } + } + .background(RoundedRectangle(cornerRadius: settings.cornerRadius).fill(settings.iPadBackgroundColor)) + .frame(width: settings.buttonSize.width) + + VStack { + Button { + defaultState() + } label: { + + HStack { + Text(settings.cancel).foregroundColor(settings.iPadImageColor) + } + .frame(width: settings.buttonSize.width, height: settings.buttonSize.height) + } + } + .background(RoundedRectangle(cornerRadius: settings.cornerRadius).fill(settings.iPadBackgroundColor)) + .frame(width: settings.buttonSize.width) + } + .frame(width: settings.buttonSize.width) + .shadow(color: settings.shadowColor, radius: settings.blurRadius) + ) + } + }) + + .imagePicker(isCameraPresented: $isCameraPresented, + mediaTypes: mediaTypes, + onDismiss: { + defaultState() + }, onGetAvatarImage: { avatarImage in + onGetAvatarImage(avatarImage) + defaultState() + }) + + .photosPicker(isPresented: $isImagePickerPresented, selection: $selectedItem, + matching: .any(of: mediaTypes), + photoLibrary: .shared()) + + .onChange(of: selectedItem) { _ in + Task { + self.avatarImage = nil + + if let data = try? await selectedItem?.loadTransferable(type: Data.self), + let contentType = selectedItem?.supportedContentTypes.first { + let url = documentsDirectoryPath().appendingPathComponent("\(UUID().uuidString).\(contentType.preferredFilenameExtension ?? "")") + do { + try data.write(to: url) + if let avatarImage = UIImage(data: data) { + self.avatarImage = avatarImage + } + } catch { + print("Failed") + } + } + print("Failed") + } + } + } + } + + private func defaultState() { + isAlertPresented = false + isCameraPresented = false + isImagePickerPresented = false + } +} + +extension View { + func mediaAlert( + isAlertPresented: Binding, + isExistingImage: Bool, + mediaTypes: [PHPickerFilter], + viewModel: EnterViewModel, + onRemoveImage: @escaping () -> Void, + onGetAvatarImage: @escaping (_ avatarImage: UIImage) -> Void + ) -> some View { + self.modifier(CustomMediaAlert(viewModel: viewModel, + isAlertPresented: isAlertPresented, + isExistingImage: isExistingImage, + mediaTypes: mediaTypes, + onRemoveImage: onRemoveImage, + onGetAvatarImage: onGetAvatarImage + )) + } +} + +public enum MediaPickerAction: CaseIterable { + case removePhoto, camera, photo +} + +public struct MediaPickerSegmentView: View { + public var settings = QuickBloxUIKit.settings.dialogNameScreen.mediaAlert + + let action: MediaPickerAction + let onTap: (_ action: MediaPickerAction) -> Void + + @ViewBuilder + public var body: some View { + Button { + onTap(action) + } label: { + HStack { + switch action { + case .removePhoto: + Text(settings.removePhoto).foregroundColor(settings.removePhotoColor) + Spacer() + settings.imageClose.foregroundColor(settings.removePhotoColor) + case .camera: + Text(settings.camera).foregroundColor(settings.iPadForegroundColor) + Spacer() + settings.imageCamera.foregroundColor(settings.iPadImageColor) + case .photo: + Text(settings.gallery).foregroundColor(settings.iPadForegroundColor) + Spacer() + settings.imageGallery.foregroundColor(settings.iPadImageColor) + } + } + .padding() + } + .frame(width: settings.buttonSize.width, height: settings.buttonSize.height) + } +} + +public struct ImagePicker: ViewModifier { + + @Binding var isCameraPresented: Bool + @State var avatarImage: UIImage? = nil + var mediaTypes: [PHPickerFilter] + let onDismiss: () -> Void + let onGetAvatarImage: (_ image: UIImage) -> Void + + public func body(content: Content) -> some View { + ZStack { + content + .fullScreenCover(isPresented: $isCameraPresented) { + ZStack { + if isCameraPresented == true { + Color.black.ignoresSafeArea(.all) + } + MediaPickerView(sourceType: UIImagePickerController.SourceType.camera, + avatarImage: $avatarImage, + isPresented: $isCameraPresented, + mediaTypes:convert(mediaTypes)) + .onDisappear { + onDismiss() + if let avatarImage { + onGetAvatarImage(avatarImage) + } + } + + } + } + } + } +} + +private func convert(_ mediaTypes: [PHPickerFilter]) -> [String] { + var mediaIdentifiers: [String] = [] + for type in mediaTypes { + switch type { + case .videos: + mediaIdentifiers.append(UTType.movie.identifier) + case .images: + mediaIdentifiers.append(UTType.image.identifier) + default: continue + } + } + return mediaIdentifiers +} + +extension View { + func imagePicker( + isCameraPresented: Binding, + mediaTypes: [PHPickerFilter], + onDismiss: @escaping () -> Void, + onGetAvatarImage: @escaping (_ image: UIImage) -> Void + ) -> some View { + self.modifier(ImagePicker(isCameraPresented: isCameraPresented, + mediaTypes: mediaTypes, + onDismiss: onDismiss, + onGetAvatarImage: onGetAvatarImage)) + } +} + + +func documentsDirectoryPath() -> URL { + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + return paths[0] +} + +struct MediaPickerView: UIViewControllerRepresentable { + + var sourceType: UIImagePickerController.SourceType = .photoLibrary + @Binding var avatarImage: UIImage? + @Binding var isPresented: Bool + var mediaTypes: [String] + + func makeCoordinator() -> MediaPickerViewCoordinator { + return MediaPickerViewCoordinator(avatarImage: $avatarImage, isPresented: $isPresented) + } + + func makeUIViewController(context: Context) -> UIImagePickerController { + let pickerController = UIImagePickerController() + pickerController.sourceType = sourceType + pickerController.delegate = context.coordinator + pickerController.mediaTypes = mediaTypes + pickerController.allowsEditing = true + return pickerController + } + + func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {} +} + +class MediaPickerViewCoordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { + + @Binding var avatarImage: UIImage? + @Binding var isPresented: Bool + + init(avatarImage: Binding, isPresented: Binding) { + self._avatarImage = avatarImage + self._isPresented = isPresented + } + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + var image : UIImage = UIImage() + if let editedImage = info[.editedImage] as? UIImage { + image = editedImage + } else if let originalImage = info[.originalImage] as? UIImage { + image = originalImage + } + + self.avatarImage = image + self.isPresented = false + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + self.isPresented = false + } + + private func convertFromUIImagePickerControllerInfoKeyDictionary(_ input: [UIImagePickerController.InfoKey: Any]) -> [String: Any] { + return Dictionary(uniqueKeysWithValues: input.map {key, value in (key.rawValue, value)}) + } +} + +struct CustomProgressView: View { + let progressBar = QuickBloxUIKit.settings.dialogsScreen.progressBar + + var body: some View { + ZStack { + Color.black.frame(width: 100, height: 100) + .cornerRadius(12) + .opacity(0.6) + + SegmentedCircularBar(settings: progressBar) + } + } +} + +struct SegmentedCircularBar: View { + var settings: ProgressBarSettingsProtocol + @State private var currentSegment = 0 + + private var totalEmptySpaceAngle: Angle { + settings.emptySpaceAngle * Double(settings.segments) + } + + private var availableAngle: Angle { + Angle(degrees: 360.0) - totalEmptySpaceAngle + } + + private var segmentAngle: Angle { + availableAngle / Double(settings.segments) + } + + var body: some View { + ZStack { + ForEach(0.. some View { + let startAngle = Angle(degrees: Double(index) * (segmentAngle.degrees + settings.emptySpaceAngle.degrees)) + let endAngle = Angle(degrees: startAngle.degrees + segmentAngle.degrees) + + return Circle() + .trim(from: CGFloat(startAngle.radians / (2 * .pi)), to: CGFloat(endAngle.radians / (2 * .pi))) + .stroke(segmentColor(at: index), + style: StrokeStyle(lineWidth: settings.lineWidth, lineCap: .butt)) + } + + private func segmentColor(at index: Int) -> Color { + return index == currentSegment || index == nextIndex ? settings.progressSegmentColor : settings.segmentColor + } + + var nextIndex: Int { + let next = currentSegment + 1 + if next == settings.segments { + return 0 + } + return next + } + + func startAnimation() { + withAnimation { + if currentSegment < settings.segments - 1 { + currentSegment = currentSegment + 1 + } else { + currentSegment = 0 + } + } + DispatchQueue.main.asyncAfter(deadline: .now() + settings.segmentDuration) { + startAnimation() + } + } +} + +struct SegmentedCircularBarContentView: View { + let settings = QuickBloxUIKit.settings.dialogScreen.messageRow + + var body: some View { + VStack { + + ZStack { + + settings.progressBarBackground() + .frame(width: settings.attachmentSize.width, + height: settings.attachmentSize.height) + .cornerRadius(settings.attachmentRadius, corners: settings.inboundCorners) + .padding(settings.inboundPadding(showName: settings.isHiddenName)) + + SegmentedCircularBar(settings: settings.aiProgressBar) + + } + } + } +} + +struct SegmentedCircularBarContentView_Previews: PreviewProvider { + static var previews: some View { + SegmentedCircularBarContentView() + SegmentedCircularBarContentView() + .preferredColorScheme(.dark) + } +} + diff --git a/Q-municate/Q-municate/View/EnterPhoneNumberView.swift b/Q-municate/Q-municate/View/EnterPhoneNumberView.swift new file mode 100644 index 000000000..6acf35a21 --- /dev/null +++ b/Q-municate/Q-municate/View/EnterPhoneNumberView.swift @@ -0,0 +1,289 @@ +// +// EnterPhoneNumberView.swift +// Q-municate +// +// Created by Injoit on 11.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import SwiftUI + +struct EnterPhoneNumberViewConstant { + static let privacyPolicyPath = "" + static let termsOfServicePath = "" +} + +struct EnterPhoneNumberView: View { + @State private var settings: EnterPhoneNumberScreenSettings + private var theme: AppTheme + @State private var isFailureAlertPresented: Bool = false + @State private var isVerifyCodeFailureAlertPresented: Bool = false + + @ObservedObject private var viewModel: EnterViewModel + + init(theme: AppTheme, viewModel: EnterViewModel) { + self.settings = EnterPhoneNumberScreenSettings(theme) + self.theme = theme + self.viewModel = viewModel + } + + var body: some View { + NavigationStack { + ZStack(alignment: .center) { + settings.backgroundColor.ignoresSafeArea() + + VStack(alignment: .leading, spacing: 16) { + + HStack(spacing: 8) { + Text(settings.countryCode.title) + .foregroundColor(settings.countryCode.titleColor) + .font(settings.countryCode.titleFont) + .padding(.trailing, settings.countryCode.tralingSpacing) + + NavigationLink { + CountryView(viewModel: viewModel, settings: settings.countryScreen) + } label: { + Text(viewModel.selectedCountry.flag) + .font(settings.countryCode.iconFont) + + Text(viewModel.selectedCountry.dial_code + " (\(viewModel.selectedCountry.name))") + .foregroundColor(settings.countryCode.codeColor) + .font(settings.countryCode.codeFont) + + Spacer() + + settings.countryCode.arrowRight + .foregroundColor(settings.countryCode.arrowColor) + .padding(.trailing, settings.countryCode.tralingSpacing) + } + + }.frame(height: 22.0) + + Divider() + + HStack { + Text(settings.phoneNumber.title) + .foregroundColor(settings.phoneNumber.titleColor) + .font(settings.phoneNumber.titleFont) + .padding(.trailing, settings.countryCode.tralingSpacing) + + PhoneNumberTextField(phoneNumber: $viewModel.phoneNumber, settings: settings) + + }.frame(height: 22.0) + + Spacer() + + HStack(spacing: 16) { + Spacer() + NavigationLink { + TermsView(urlString: EnterPhoneNumberViewConstant.privacyPolicyPath, + termsType: .pravicy, + settings: settings.terms) + } label: { + Text(settings.terms.privacyPolicy) + .foregroundColor(settings.terms.color) + .font(settings.terms.font) + } + + NavigationLink { + TermsView(urlString: EnterPhoneNumberViewConstant.termsOfServicePath, + termsType: .terms, + settings: settings.terms) + } label: { + Text(settings.terms.terms) + .foregroundColor(settings.terms.color) + .font(settings.terms.font) + } + + }.frame(height: 22.0) + } + .padding(.horizontal) + .padding(.top, 56) + + } + + .if(viewModel.isPhoneNumberVerifiedSuccess == true, transform: { view in + view.navigationDestination(isPresented: $viewModel.isPhoneNumberVerifiedSuccess, destination: { + VerifyPhoneNumberView(viewModel: viewModel, + isVerifyCodeFailureAlertPresented: $isVerifyCodeFailureAlertPresented, + theme: theme) + }) + }) + .disabled(viewModel.isProcessing == true) + + .if(viewModel.isProcessing == true && viewModel.isPhoneNumberVerifiedSuccess == false) { view in + view.overlay() { + CustomProgressView() + } + } + + .if(viewModel.isPhoneNumberVerifiedSuccess == false, transform: { view in + view.onChange(of: viewModel.error) { newValue in + if newValue == AuthError.networkError.localizedDescription { + return + } + if newValue.isEmpty == false && viewModel.isPhoneNumberVerifiedSuccess == false { + isFailureAlertPresented = true + } + } + }) + + .onChange(of: viewModel.verifyCodeError) { newValue in + if newValue.isEmpty == false && viewModel.isPhoneNumberVerifiedSuccess == true { + isVerifyCodeFailureAlertPresented = true + } + } + + .if(isFailureAlertPresented == true && viewModel.isPhoneNumberVerifiedSuccess == false, transform: { view in + view.enterPhoneFailureAlert(isPresented: $isFailureAlertPresented, + message: viewModel.error, + onDismiss: { + viewModel.error = "" + }, settings: settings) + }) + + .modifier(EnterPhoneNumberHeader(onTapGetCode: { + viewModel.verifyPhoneNumber() + }, disabled: viewModel.phoneNumber.isEmpty == true + || viewModel.isCodeSendingBlocked == true + || viewModel.isProcessing == true, + settings: settings.header)) + + }.accentColor(settings.header.rightButton.color) + } +} + +public struct PhoneNumberTextField: View { + private var settings: EnterPhoneNumberScreenSettings + + @Binding var phoneNumber: String + @FocusState private var focused: Bool? + + init(phoneNumber: Binding, + settings: EnterPhoneNumberScreenSettings) { + _phoneNumber = phoneNumber + self.settings = settings + } + + public var body: some View { + TextField("", text: $phoneNumber) + .focused($focused, equals: true) + .keyboardType(.numberPad) + .foregroundColor(settings.phoneNumber.numberColor) + .font(settings.phoneNumber.numberFont) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.focused = true + } + } + } +} + + +struct EnterPhoneNumberHeaderToolbarContent: ToolbarContent { + + private var settings: EnterPhoneNumberHeaderSettings + + let onTapGetCode: () -> Void + var disabled: Bool = false + + init(onTapGetCode: @escaping () -> Void, + disabled: Bool, + settings: EnterPhoneNumberHeaderSettings) { + self.onTapGetCode = onTapGetCode + self.disabled = disabled + self.settings = settings + } + + public var body: some ToolbarContent { + ToolbarItem(placement: .principal) { + Text(settings.title.text) + .font(settings.title.font) + .foregroundColor(settings.title.color) + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button { + onTapGetCode() + } label: { + Text(settings.rightButton.title) + .foregroundColor(settings.rightButton.color.opacity(disabled == false ? 1.0 : 0.4)) + }.disabled(disabled) + } + } +} + +public struct EnterPhoneNumberHeader: ViewModifier { + + private var settings: EnterPhoneNumberHeaderSettings + + private let onTapGetCode: () -> Void + private var disabled: Bool = false + + init(onTapGetCode: @escaping () -> Void, + disabled: Bool, + settings: EnterPhoneNumberHeaderSettings) { + self.onTapGetCode = onTapGetCode + self.disabled = disabled + self.settings = settings + } + + public func body(content: Content) -> some View { + content.toolbar { + EnterPhoneNumberHeaderToolbarContent(onTapGetCode: onTapGetCode, + disabled: disabled, + settings: settings) + } + .navigationTitle("") + .navigationBarTitleDisplayMode(settings.displayMode) + .navigationBarBackButtonHidden(true) + .navigationBarHidden(settings.isHidden) + .toolbarBackground(settings.backgroundColor,for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) + .toolbarRole(.editor) + } +} + +struct EnterPhoneFailureAlert: ViewModifier { + @Binding var isPresented: Bool + let message: String + let onDismiss: () -> Void + + let settings: EnterPhoneNumberScreenSettings + + func body(content: Content) -> some View { + ZStack(alignment: .center) { + content.blur(radius: isPresented ? 12.0 : 0.0) + .disabled(isPresented) + .alert("", isPresented: $isPresented) { + Button(settings.titleOk, action: { + isPresented = false + onDismiss() + }) + } message: { + Text(message) + } + } + } +} + +extension View { + func enterPhoneFailureAlert( + isPresented: Binding, + message: String, onDismiss: @escaping () -> Void, + settings: EnterPhoneNumberScreenSettings + ) -> some View { + self.modifier(EnterPhoneFailureAlert(isPresented: isPresented, + message: message, + onDismiss: onDismiss, + settings: settings)) + } +} + +public var isIphone: Bool { + UIDevice.current.userInterfaceIdiom == .phone +} + +public var isIPad: Bool { + UIDevice.current.userInterfaceIdiom == .pad +} diff --git a/Q-municate/Q-municate/View/EnterView.swift b/Q-municate/Q-municate/View/EnterView.swift new file mode 100644 index 000000000..06b229b17 --- /dev/null +++ b/Q-municate/Q-municate/View/EnterView.swift @@ -0,0 +1,226 @@ +// +// EnterView.swift +// Q-municate +// +// Created by Injoit on 19.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import SwiftUI +import QuickBloxUIKit +import QuickBloxDomain +import Combine + +struct EnterViewConstant { + static let regexUserName = "^(?=[a-zA-Z])[-a-zA-Z_ ]{3,49}(? = Set() + + init(viewModel: EnterViewModel) { + self.viewModel = viewModel + setupFeatures() + + QuickBloxUIKit.syncState + .receive(on: RunLoop.main) + .sink { syncState in + if syncState == QuickBloxDomain.SyncState.syncing(stage: .unauthorized), + viewModel.isAutoLogin == false { + viewModel.logOut() + viewModel.authState = .unAuthorized + } + } + .store(in: &cancellables) + } + + var body: some View { + ZStack { + theme.color.mainBackground.ignoresSafeArea() + + switch viewModel.authState { + case .unAuthorized: + EnterPhoneNumberView(theme: theme, viewModel: viewModel).onAppear { + selectedTabIndex = .dialogs + } + + case .authorized: + if viewModel.user?.name.isEmpty == true || + viewModel.user?.name.isValid(regexes: [EnterViewConstant.regexUserName]) == false { + CreateProfileView(viewModel, theme: theme) + .onChange(of: viewModel.userName) { newValue in + tabBarVisibility = newValue.isEmpty == false ? .visible : .hidden + } + } else { + TabView(selection: $selectedTabIndex) { + if viewModel.user?.name.isEmpty == false { + QuickBloxUIKit.dialogsView(onSelect: { tabIndex in + if tabIndex != .dialogs { + selectedTabIndex = tabIndex + } + }) + .toolbar(tabBarVisibility, for: .tabBar) + .toolbarBackground(theme.color.mainBackground, for: .tabBar) + .toolbarBackground(tabBarVisibility, for: .tabBar) + .tag(TabIndex.dialogs) + .tabItem { + Label(TabIndex.dialogs.title, systemImage: TabIndex.dialogs.systemIcon) + } + .onAppear { + theme = appThemes[UserDefaults.standard.integer(forKey: "Theme")] + setupSettings() + tabBarVisibility = selectedTabIndex == .settings ? .visible : .hidden + } + } + + if viewModel.user != nil { + CreateProfileView(viewModel, theme: theme) + .toolbar(tabBarVisibility, for: .tabBar) + .toolbarBackground(theme.color.mainBackground, for: .tabBar) + .toolbarBackground(tabBarVisibility, for: .tabBar) + .tabItem { + Label(TabIndex.settings.title, systemImage: TabIndex.settings.systemIcon) + } + .tag(TabIndex.settings) + } + } + .accentColor(theme.color.mainElements) + .onAppear { + theme = appThemes[UserDefaults.standard.integer(forKey: "Theme")] + setupSettings() + tabBarVisibility = selectedTabIndex == .settings ? .visible : .hidden + } + + .onChange(of: selectedTabIndex) { newValue in + tabBarVisibility = newValue == .settings ? .visible : .hidden + } + + .onChange(of: viewModel.error) { newValue in + showNoInternetAlert = newValue == AuthError.networkError.localizedDescription + } + + .enterPhoneFailureAlert(isPresented: $showNoInternetAlert, + message: viewModel.error, + onDismiss: { + viewModel.error = "" + showNoInternetAlert = false + }, settings: EnterPhoneNumberScreenSettings(theme)) + + } + case .checkVersion: + ZStack { + Color("backgroundLaunch").ignoresSafeArea() + ZStack { + Image("qmunicate-logo") + .resizable() + .renderingMode(.template) + .foregroundColor(.white) + .scaledToFit() + .frame(width: 178, height: 154) + VStack { + Spacer() + + Text(theme.qmunicateString.poweredByQuickblox) + .font(.system(size: 12, weight: .medium)) + .foregroundColor(.white) + } + } + } + .onChange(of: viewModel.isAvailableUpdateAppVersion) { newValue in + if newValue == true { + showAppVersionAlert = true + } + } + .if(showAppVersionAlert == true && viewModel.lastVersion.isEmpty == false) { view in + view.appVersionAlert(theme: theme, isPresented: $showAppVersionAlert, + lastVersion: viewModel.lastVersion, + onUpdate: { update in + update == true ? viewModel.openAppStore() : viewModel.startAuthFlow() + }) + } + } + } + } + + private func setupFeatures() { + QuickBloxUIKit.feature.ai.apiKey = EnterViewConstant.aiApiKey + QuickBloxUIKit.feature.ai.ui = QuickBloxUIKit.AIUISettings(theme) + QuickBloxUIKit.feature.forward.enable = true + QuickBloxUIKit.feature.reply.enable = true + QuickBloxUIKit.feature.regex.userName = EnterViewConstant.regexUserName + QuickBloxUIKit.feature.regex.dialogName = EnterViewConstant.regexDialogName + QuickBloxUIKit.feature.toolbar.enable = true + } + + private func setupSettings() { + // Setup Custom Theme + QuickBloxUIKit.settings.theme = theme + + // Hide backButton for Dialogs Screen + QuickBloxUIKit.settings.dialogsScreen.header.leftButton.hidden = true + + // Setup Background Image for Dialog Screen + QuickBloxUIKit.settings.dialogScreen.backgroundImage = Image("dialogBackground") + QuickBloxUIKit.settings.dialogScreen.backgroundImageColor = theme.color.divider + } +} + +struct AppVersionAlert: ViewModifier { + @State private var settings: UpdateVersionAlertSettings + @Binding var isPresented: Bool + let lastVersion: String + let onUpdate: (_ isUpdate: Bool) -> Void + + init(theme: AppTheme, + isPresented: Binding, + lastVersion: String, + onUpdate: @escaping (_: Bool) -> Void) { + self.settings = UpdateVersionAlertSettings(theme) + _isPresented = isPresented + self.lastVersion = lastVersion + self.onUpdate = onUpdate + } + + func body(content: Content) -> some View { + ZStack(alignment: .center) { + content.blur(radius: 12.0) + .disabled(true) + .alert(settings.newVersion, isPresented: $isPresented) { + Button(settings.skip, action: { + onUpdate(false) }) + Button(settings.update, action: { + onUpdate(true) + }) + } message: { + Text(settings.updateToVersion + " " + lastVersion) + } + } + } +} + +extension View { + func appVersionAlert( + theme: AppTheme, + isPresented: Binding, + lastVersion: String, + onUpdate: @escaping (_ isUpdate: Bool) -> Void + ) -> some View { + self.modifier(AppVersionAlert(theme: theme, + isPresented: isPresented, + lastVersion: lastVersion, + onUpdate: onUpdate)) + } +} diff --git a/Q-municate/Q-municate/View/LaunchScreen.storyboard b/Q-municate/Q-municate/View/LaunchScreen.storyboard new file mode 100644 index 000000000..9604134cf --- /dev/null +++ b/Q-municate/Q-municate/View/LaunchScreen.storyboard @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Q-municate/Q-municate/View/SelectCountryListView.swift b/Q-municate/Q-municate/View/SelectCountryListView.swift new file mode 100644 index 000000000..e62a7309f --- /dev/null +++ b/Q-municate/Q-municate/View/SelectCountryListView.swift @@ -0,0 +1,142 @@ +// +// SelectCountryListView.swift +// Q-municate +// +// Created by Injoit on 12.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import SwiftUI +import QuickBloxUIKit + +struct SelectCountryListView: View { + @Environment(\.isSearching) private var isSearching: Bool + @Environment(\.dismiss) var dismiss + + private var settings: CountryScreenSettings + + @ObservedObject private var viewModel: EnterViewModel + @State private var searchText = "" + @State private var submittedSearchTerm = "" + + private var items: [CountryPhoneCode] { + if settings.searchBar.isSearchable == false || submittedSearchTerm.isEmpty { + return viewModel.countryPhoneCodes + } else { + return viewModel.countryPhoneCodes.filter { $0.name.lowercased() + .contains(submittedSearchTerm.lowercased()) } + } + } + + init(viewModel: EnterViewModel, + settings: CountryScreenSettings) { + self.viewModel = viewModel + self.settings = settings + } + + var body: some View { + ZStack { + settings.backgroundColor.ignoresSafeArea() + List { + ForEach(items) { item in + ZStack { + CountryRowView(item, isSelected: item.code == viewModel.selectedCountry.code, onTap: {item in + viewModel.selectCountry(item) + dismiss() + }, settings: settings.countryRow) + } + .alignmentGuide(.listRowSeparatorLeading) { viewDimensions in + return items.last?.id == item.id ? viewDimensions[.leading] + : viewDimensions[.listRowSeparatorLeading] + } + } + .listRowInsets(EdgeInsets()) + }.listStyle(.plain) + } + + .if(settings.searchBar.isSearchable, + transform: { view in + view.searchable(text: $searchText, + placement: .navigationBarDrawer(displayMode: .always), + prompt: settings.searchBar.searchTextField.placeholderText) + .onChange(of: searchText) { value in + if searchText.isEmpty && !isSearching { + submittedSearchTerm = "" + } else { + submittedSearchTerm = searchText + } + } + .autocorrectionDisabled(true) + }) + } +} + +struct CountryRowView: View { + + private var settings: CountryRowSettings + + public var countryCode: CountryPhoneCode + + private var isSelected = false + public var onTap: (_ countryCode: CountryPhoneCode) -> Void + + public init(_ countryCode: CountryPhoneCode, + isSelected: Bool, + onTap: @escaping (_ countryCode: CountryPhoneCode) -> Void, + settings: CountryRowSettings) { + self.countryCode = countryCode + self.isSelected = isSelected + self.onTap = onTap + self.settings = settings + } + + public var body: some View { + HStack(spacing: 16) { + Text(countryCode.flag).font(settings.iconFont) + + Text(countryCode.dial_code + " (\(countryCode.name))") + .foregroundColor(settings.codeColor) + .font(settings.codeFont) + + Spacer() + + Checkbox(isSelected: isSelected) { + onTap(countryCode) + } + } + .frame(height: settings.selectHeight) + .padding(settings.selectPadding) + .background(settings.backgroundColor) + } +} + +struct Checkbox: View { + public var settings = QuickBloxUIKit.settings.createDialogScreen.userRow.checkbox + + public var isSelected: Bool + public var font: Font? = nil + public var foregroundColor: Color? = nil + public var backgroundColor: Color? = nil + public var onTap: (() -> Void)? + + var body: some View { + Button { + onTap?() + } label: { + if isSelected { + settings.selected + .font(font ?? settings.font) + .foregroundColor(foregroundColor ?? settings.foregroundColorSelected) + .frame(width: settings.heightSelected, height: settings.heightSelected) + .background(backgroundColor ?? settings.backgroundColor) + .scaledToFit() + .clipShape(Circle()) + } else { + Circle() + .strokeBorder(settings.strokeBorder, lineWidth: settings.lineWidth) + .frame(width: settings.heightSelected, height: settings.heightSelected) + } + } + .frame(width: settings.heightButton, height: settings.heightButton) + } +} diff --git a/Q-municate/Q-municate/View/TermsView.swift b/Q-municate/Q-municate/View/TermsView.swift new file mode 100644 index 000000000..c28a51c1a --- /dev/null +++ b/Q-municate/Q-municate/View/TermsView.swift @@ -0,0 +1,76 @@ +// +// TermsView.swift +// Q-municate +// +// Created by Injoit on 13.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import SwiftUI +import WebKit + +struct TermsView: View { + private var settings: TermsSettings + + private var urlString = EnterPhoneNumberViewConstant.privacyPolicyPath + private var termsType: TermsType + + init(urlString: String, + termsType: TermsType, + settings: TermsSettings) { + self.urlString = urlString + self.termsType = termsType + self.settings = settings + } + + var body: some View { + CustomWebView(urlString: urlString) + .modifier(TermsHeader(termsType: termsType, settings: settings)) + } +} + +struct CustomWebView: UIViewRepresentable { + + let webView: WKWebView + var urlString = EnterPhoneNumberViewConstant.privacyPolicyPath + + init(urlString: String) { + self.webView = WKWebView() + self.urlString = urlString + } + + func makeUIView(context: Context) -> WKWebView { + webView.allowsBackForwardNavigationGestures = true + return webView + } + + func updateUIView(_ uiView: WKWebView, context: Context) { + if let url = URL(string: urlString) { + webView.load(URLRequest(url: url)) + } + } +} + +public struct TermsHeader: ViewModifier { + private var settings: TermsSettings + private var termsType: TermsType + + init(termsType: TermsType, settings: TermsSettings) { + self.settings = settings + self.termsType = termsType + } + + public func body(content: Content) -> some View { + content + .navigationTitle(termsType == .pravicy ? settings.privacyPolicy : settings.terms) + .navigationBarTitleDisplayMode(settings.header.displayMode) + .navigationBarBackButtonHidden(false) + .navigationBarHidden(settings.header.isHidden) + .toolbarBackground(settings.header.backgroundColor,for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) + } +} + +enum TermsType { + case pravicy, terms +} diff --git a/Q-municate/Q-municate/View/VerifyPhoneNumberView.swift b/Q-municate/Q-municate/View/VerifyPhoneNumberView.swift new file mode 100644 index 000000000..2b12c03da --- /dev/null +++ b/Q-municate/Q-municate/View/VerifyPhoneNumberView.swift @@ -0,0 +1,301 @@ +// +// VerifyPhoneNumberView.swift +// Q-municate +// +// Created by Injoit on 13.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import SwiftUI +import Combine + +struct VerifyPhoneNumberView: View { + @Environment(\.dismiss) var dismiss + private var settings: VerifyPhoneNumberScreenSettings + private var theme: AppTheme + + @State private var isVerifyCodeTapped: Bool = false + @Binding private var isVerifyCodeFailureAlertPresented: Bool + @FocusState private var isTextFieldFocused: Bool + + @ObservedObject private var viewModel: EnterViewModel + + init(viewModel: EnterViewModel, + isVerifyCodeFailureAlertPresented: Binding, + theme: AppTheme) { + self.viewModel = viewModel + + self.settings = VerifyPhoneNumberScreenSettings(theme) + self.theme = theme + _isVerifyCodeFailureAlertPresented = isVerifyCodeFailureAlertPresented + } + + var body: some View { + ZStack(alignment: .center) { + settings.backgroundColor.ignoresSafeArea() + + VStack(spacing: 0) { + + Text(settings.enterCodeLabel.title) + .foregroundColor(settings.enterCodeLabel.titleColor) + .font(settings.enterCodeLabel.titleFont) + + Text(viewModel.selectedCountry.dial_code + " \(viewModel.phoneNumber)") + .foregroundColor(settings.enterCodeLabel.numberColor) + .font(settings.enterCodeLabel.numberFont) + + SMSCodeEnterFieldView(numberOfFields: 6, smsCode: $viewModel.smsCode, settings: settings) + .onChange(of: viewModel.smsCode) { newSMSCode in + if newSMSCode.count > 6 { + viewModel.smsCode = String(viewModel.smsCode.prefix(6)) + } + } + .padding(.vertical, 36) + .focused($isTextFieldFocused, equals: true) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + isTextFieldFocused = true + } + } + + if viewModel.isCodeSendingBlocked == false { + Button { + if viewModel.isCodeSendingBlocked == false { + viewModel.verifyPhoneNumber() + } + } label: { + Text(settings.resendCode.title) + .foregroundColor(viewModel.isCodeSendingBlocked == false ? settings.resendCode.color : settings.resendCode.color.opacity(0.4)) + .font(settings.resendCode.font) + } + .disabled(viewModel.isCodeSendingBlocked == true) + } else { + Text("Re-send in 00:\(timeFormatted(viewModel.totalResendTime))") + .foregroundColor(settings.enterCodeLabel.numberColor) + .font(settings.enterCodeLabel.numberFont) + } + + Spacer() + + } + .padding(.horizontal) + .padding(.top, 56) + } + + .if(viewModel.isVerifyCodeProcessing == true) { view in + view.overlay() { + CustomProgressView() + } + } + + .if(isVerifyCodeFailureAlertPresented == true, transform: { view in + view.enterPhoneFailureAlert(isPresented: $isVerifyCodeFailureAlertPresented, + message: viewModel.verifyCodeError, + onDismiss: { + viewModel.verifyCodeError = "" + }, settings: EnterPhoneNumberScreenSettings(theme)) + }) + + .disabled(viewModel.isVerifyCodeProcessing == true) + .modifier(EnterCodeHeader(onDismiss: { + viewModel.isPhoneNumberVerifiedSuccess = false + viewModel.invalidateResendCodeTimer() + dismiss() + }, onTapVerifyCode: { + viewModel.verifyCode() + }, disabled: viewModel.smsCode.count != 6 || viewModel.isVerifyCodeProcessing == true, + settings: settings.header)) + } +} + + +struct EnterCodeHeaderToolbarContent: ToolbarContent { + private var settings: VerifyPhoneNumberHeaderSettings + + private let onDismiss: () -> Void + private let onTapVerifyCode: () -> Void + private var disabled: Bool = false + + init(onDismiss: @escaping () -> Void, + onTapVerifyCode: @escaping () -> Void, + disabled: Bool, + settings: VerifyPhoneNumberHeaderSettings) { + self.onDismiss = onDismiss + self.onTapVerifyCode = onTapVerifyCode + self.disabled = disabled + self.settings = settings + } + + public var body: some ToolbarContent { + ToolbarItem(placement: .navigationBarLeading) { + Button { + onDismiss() + } label: { + settings.leftButton.image + .resizable() + .scaledToFit() + .scaleEffect(settings.leftButton.scale) + .tint(settings.leftButton.color) + .padding(settings.leftButton.padding) + + }.frame(width: 32, height: 44) + } + + ToolbarItem(placement: .principal) { + Text(settings.title.text) + .font(settings.title.font) + .foregroundColor(settings.title.color) + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button { + onTapVerifyCode() + } label: { + Text(settings.rightButton.title) + .foregroundColor(settings.rightButton.color.opacity(disabled == false ? 1.0 : 0.4)) + }.disabled(disabled) + } + } +} + +public struct EnterCodeHeader: ViewModifier { + private var settings: VerifyPhoneNumberHeaderSettings + + private let onDismiss: () -> Void + private let onTapVerifyCode: () -> Void + private var disabled: Bool = false + + init(onDismiss: @escaping () -> Void, + onTapVerifyCode: @escaping () -> Void, + disabled: Bool, + settings: VerifyPhoneNumberHeaderSettings) { + self.onDismiss = onDismiss + self.onTapVerifyCode = onTapVerifyCode + self.disabled = disabled + self.settings = settings + } + + public func body(content: Content) -> some View { + content.toolbar { + EnterCodeHeaderToolbarContent(onDismiss: onDismiss, onTapVerifyCode: onTapVerifyCode, disabled: disabled, settings: settings) + } + .navigationTitle("") + .navigationBarTitleDisplayMode(settings.displayMode) + .navigationBarBackButtonHidden(true) + .navigationBarHidden(settings.isHidden) + .toolbarBackground(settings.backgroundColor,for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) + .toolbarRole(.editor) + } +} + +struct SMSCodeEnterFieldView: View { + private var settings: VerifyPhoneNumberScreenSettings + + @Binding private var smsCode: String + @State private var digits: [String] + @FocusState private var digitFocusState: FocusDigit? + + var numberOfFields: Int + + enum FocusDigit: Hashable { + case digit(Int) + } + + init(numberOfFields: Int, smsCode: Binding, + settings: VerifyPhoneNumberScreenSettings) { + self.numberOfFields = numberOfFields + self._smsCode = smsCode + self.settings = settings + self._digits = State(initialValue: Array(repeating: "", count: numberOfFields)) + } + + var body: some View { + HStack(spacing: 15) { + ForEach(0.. 0 { + digitFocusState = FocusDigit.digit(index - 1) + } + } + setupSMSCode() + } + .focused($digitFocusState, equals: FocusDigit.digit(index)) + .onTapGesture { + digitFocusState = FocusDigit.digit(index) + } + } + } + .onAppear { + setupDigits() + } + } + + private func setupSMSCode() { + smsCode = digits.joined() + } + + private func setupDigits() { + let digitsArray = Array(smsCode.prefix(numberOfFields)) + for (index, char) in digitsArray.enumerated() { + digits[index] = String(char) + } + digitFocusState = FocusDigit.digit(0) + } +} + +struct DigitalCodeModifier: ViewModifier { + @Binding var digit: String + + var textLimit = 1 + + func limitText(_ upper: Int) { + if digit.count > upper { + self.digit = String(digit.prefix(upper)) + } + } + + func body(content: Content) -> some View { + content + .multilineTextAlignment(.center) + .keyboardType(.numberPad) + .onReceive(Just(digit)) { _ in limitText(textLimit) } + .frame(width: 26, height: 48) + .font(.largeTitle) + .background( + RoundedRectangle(cornerRadius: 2) + .stroke(Color.clear, lineWidth: 1) + ) + .overlay { + if digit.isEmpty { + Rectangle().frame(width: 26, height: 2).background(.green) + } + } + } +} + +private func timeFormatted(_ totalSeconds: Int) -> String { + let seconds: Int = totalSeconds % 60 + return String(format: "%02d", seconds) +} diff --git a/Q-municate/Q-municate/ViewModel/EnterViewModel.swift b/Q-municate/Q-municate/ViewModel/EnterViewModel.swift new file mode 100644 index 000000000..1286d8fff --- /dev/null +++ b/Q-municate/Q-municate/ViewModel/EnterViewModel.swift @@ -0,0 +1,401 @@ +// +// EnterViewModel.swift +// Q-municate +// +// Created by Injoit on 12.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import SwiftUI +import Combine + +struct EnterViewModelConstant { + static let allowedPhoneNumChar = "0123456789+" + static let networkErrorCode = 17020 + static let invalidPhoneNumberCode = 17042 + static let invalidVerificationCode = 17044 + static let noInternetConnectionCode = -1009 + static let totalResendTime = 60 + static let smsCodeCount = 6 +} + +enum AuthState { + case authorized + case unAuthorized + case checkVersion +} + +final class EnterViewModel: ObservableObject { + @Published public var avatar: UIImage? = nil + @Published public var isLoadingAvatar: Bool = false + @Published public var userName = "" + @Published public var error = "" + @Published public var verifyCodeError = "" + @Published public var isValidUserName = false + @Published public var isProcessing: Bool = false + @Published public var isAutoLogin: Bool = false + + @Published public var isVerifyCodeProcessing: Bool = false + @Published public var isAvailableUpdateAppVersion: Bool = false + var lastVersion: String = "" + + private var isNeedUpdateAvatar: Bool = false + + public var isExistingImage: Bool { + if let user { + return user.avatarPath.isEmpty == false + } else {} + return false + } + + @Published var isPhoneNumberVerifiedSuccess: Bool = false + + @Published var authState: AuthState = .checkVersion + var countryPhoneCodes: [CountryPhoneCode] = [] + @Published var phoneNumber: String = "" + @Published var selectedCountry: CountryPhoneCode = CountryPhoneCode.defaultCountryPhoneCode + @Published var smsCode: String = "" + @Published var isCodeSendingBlocked: Bool = false + + private var cancellables = Set() + private var resendCodeTimer: Timer? + @Published var totalResendTime = EnterViewModelConstant.totalResendTime + + var user: User? { + didSet { + if user != nil { + authState = .authorized + userName = user?.name ?? "" + } else { + authState = .unAuthorized + userName = "" + avatar = nil + } + } + } + + init() { + + checkAppStoreUpdateAvailable() + + countryPhoneCodes = CountryPhoneCode.getCodes() + + if let loadedCountryPhoneCode = loadCountryPhoneCode() { + selectedCountry = loadedCountryPhoneCode + } + + isProfileNameValidPublisher + .receive(on: RunLoop.main) + .assign(to: \.isValidUserName, on: self) + .store(in: &cancellables) + } + + func openAppStore() { + let api = API() + api.openAppStore() + authState = api.currentUser() == nil ? .unAuthorized : .authorized + } + + func checkAppStoreUpdateAvailable() { + let api = API() + do { + try api.isUpdateAvailable { [weak self] (lastVersion, _) in + if let lastVersion, lastVersion.isEmpty == false { + print("lastVersion: \(lastVersion)") + DispatchQueue.main.async { + self?.lastVersion = lastVersion + self?.isAvailableUpdateAppVersion = true + } + } else { + DispatchQueue.main.async { + self?.startAuthFlow() + } + } + } + } catch { + startAuthFlow() + print(error) + } + } + + func startAuthFlow() { + let api = API() + if let currentUser = api.currentUser() { + user = currentUser + getAvatar() + } else { + authState = .unAuthorized + return + } + if isAutoLogin == true { + return + } + isAutoLogin = true + api.autoLogin { [weak self] user, error in + self?.error = "" + if let user { + self?.isAutoLogin = false + self?.user = user + } else if let error { + if error._code == EnterViewModelConstant.networkErrorCode || + error._code == EnterViewModelConstant.noInternetConnectionCode { + DispatchQueue.main.async { + self?.error = AuthError.networkError.localizedDescription + } + self?.isAutoLogin = false + return + } + self?.isAutoLogin = false + self?.logOut() + self?.authState = .unAuthorized + } + } + } + + func selectCountry(_ country: CountryPhoneCode) { + selectedCountry = country + } + + func verifyPhoneNumber() { + if isProcessing == true { return } + + guard phoneNumber.isEmpty == false else { + error = AuthError.noPhoneNumberEntered.localizedDescription + return + } + isProcessing = true + smsCode = "" + + let phoneNumStr = (selectedCountry.dial_code + phoneNumber).filter(EnterViewModelConstant.allowedPhoneNumChar.contains) + + let api = API() + api.verifyPhoneNumber(phoneNumStr) { [weak self] error in + self?.smsCode = "" + self?.activateResendCodeTimer() + self?.isProcessing = false + if let error { + if error._code == EnterViewModelConstant.invalidPhoneNumberCode { + DispatchQueue.main.async { + self?.error = AuthError.invalidPhoneNumber.localizedDescription + } + + } else if error._code == EnterViewModelConstant.networkErrorCode || + error._code == EnterViewModelConstant.noInternetConnectionCode { + DispatchQueue.main.async { + self?.error = AuthError.networkError.localizedDescription + } + + } else { + DispatchQueue.main.async { + self?.error = error.localizedDescription + } + + } + self?.invalidateResendCodeTimer() + return + } + self?.error = "" + DispatchQueue.main.async { + self?.isPhoneNumberVerifiedSuccess = true + } + + if let loadedCountryPhoneCode = self?.loadCountryPhoneCode(), + loadedCountryPhoneCode.dial_code != self?.selectedCountry.dial_code { + self?.setCountryPhoneCode() + } else { + self?.setCountryPhoneCode() + } + } + } + + func setupUserInfo() { + userName = user?.name ?? "" + if avatar != nil, isNeedUpdateAvatar == false { return } + getAvatar() + } + + func resetUserInfo() { + isNeedUpdateAvatar = false + userName = user?.name ?? "" + getAvatar() + } + + func setupDefaultInfo() { + phoneNumber = "" + + if let loadedCountryPhoneCode = loadCountryPhoneCode() { + selectedCountry = loadedCountryPhoneCode + return + } + selectedCountry = CountryPhoneCode.defaultCountryPhoneCode + } + + private func loadCountryPhoneCode() -> CountryPhoneCode? { + let decoder = JSONDecoder() + if let savedCountryPhoneCode = UserDefaults.standard.object(forKey: FirebaseAPIConstant.countryPhoneCodeKey) as? Data, + let loadedCountryPhoneCode = try? decoder.decode(CountryPhoneCode.self, from: savedCountryPhoneCode) { + return loadedCountryPhoneCode + } + return nil + } + + private func setCountryPhoneCode() { + let encoder = JSONEncoder() + if let encoded = try? encoder.encode(selectedCountry) { + let defaults = UserDefaults.standard + defaults.set(encoded, forKey: FirebaseAPIConstant.countryPhoneCodeKey) + } + } + + func verifyCode() { + if smsCode.count != EnterViewModelConstant.smsCodeCount { + verifyCodeError = AuthError.unvalidCode.localizedDescription + return + } + + isVerifyCodeProcessing = true + verifyCodeError = "" + + let api = API() + api.verifyCode(smsCode) { [weak self] user, error in + if let error { + if error._code == EnterViewModelConstant.invalidVerificationCode{ + self?.verifyCodeError = AuthError.verifyCodeError.localizedDescription + } else { + self?.verifyCodeError = error.localizedDescription + } + self?.isVerifyCodeProcessing = false + return + } + self?.isPhoneNumberVerifiedSuccess = false + self?.isVerifyCodeProcessing = false + self?.smsCode = "" + self?.invalidateResendCodeTimer() + self?.error = "" + self?.user = user + self?.setupDefaultInfo() + } + } + + func logOut() { + let api = API() + api.logOut { [weak self] error in + if let error { + self?.error = error.localizedDescription + return + } + self?.user = nil + self?.isPhoneNumberVerifiedSuccess = false + } + } + + public func handleOnSelect(_ uiImage: UIImage) { + isNeedUpdateAvatar = true + avatar = uiImage + } + + public func removeExistingImage() { + guard let user, let blobId = UInt(user.avatarPath) else { return } + isProcessing = true + isNeedUpdateAvatar = true + let api = API() + api.deleteAvatar(blobId) { [weak self] error in + if error != nil { + self?.isProcessing = false + return + } + self?.avatar = nil + self?.updateUser() + } + } + + public func updateUser() { + if isValidUserName == false { return } + if user?.name == userName, isNeedUpdateAvatar == false { return } + isProcessing = true + let api = API() + let update = UpdateUserParameters(name: userName, avatar: avatar) + api.updateUser(update) { [weak self] user, error in + if let error { + self?.error = error.localizedDescription + self?.isProcessing = false + return + } + self?.error = "" + self?.user = user + self?.isProcessing = false + } + } + + private func getAvatar() { + guard let user = user, + user.avatarPath.isEmpty == false, + let blobId = UInt(user.avatarPath) else { return } + + isLoadingAvatar = true + + let api = API() + api.getAvatar(blobId) { [weak self] avatar, error in + if let error { + self?.error = error.localizedDescription + self?.isProcessing = false + self?.isLoadingAvatar = false + return + } + if let avatar { + DispatchQueue.main.async { + self?.avatar = avatar + self?.isLoadingAvatar = false + } + } + self?.error = "" + self?.isProcessing = false + } + } + + private func activateResendCodeTimer() { + totalResendTime = EnterViewModelConstant.totalResendTime + isCodeSendingBlocked = true + resendCodeTimer = Timer.scheduledTimer(timeInterval: 1, + target: self, + selector: #selector(updateResendTime), + userInfo: nil, + repeats: true) + } + + @objc func updateResendTime() { + if totalResendTime != 0 { + totalResendTime -= 1 + } else { + invalidateResendCodeTimer() + } + } + + @objc func invalidateResendCodeTimer() { + resendCodeTimer?.invalidate() + resendCodeTimer = nil + isCodeSendingBlocked = false + } +} + +private extension EnterViewModel { + var isProfileNameValidPublisher: AnyPublisher { + $userName + .map { userName in + return userName.isValid(regexes: [EnterViewConstant.regexUserName]) + } + .eraseToAnyPublisher() + } +} + +struct UpdateUserParameters { + let name: String + let avatar: UIImage? +} + +struct FileContent { + let data: Data + let name: String + let mimeType: String + let isPublic: Bool +} diff --git a/Q-municate/Q-municateTests/Q_municateTests.swift b/Q-municate/Q-municateTests/Q_municateTests.swift new file mode 100644 index 000000000..37212664c --- /dev/null +++ b/Q-municate/Q-municateTests/Q_municateTests.swift @@ -0,0 +1,37 @@ +// +// Q_municateTests.swift +// Q-municateTests +// +// Created by Injoit on 11.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import XCTest +@testable import Q_municate + +final class Q_municateTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/Q-municate/Q-municateUITests/Q_municateUITests.swift b/Q-municate/Q-municateUITests/Q_municateUITests.swift new file mode 100644 index 000000000..8962f2516 --- /dev/null +++ b/Q-municate/Q-municateUITests/Q_municateUITests.swift @@ -0,0 +1,42 @@ +// +// Q_municateUITests.swift +// Q-municateUITests +// +// Created by Injoit on 11.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import XCTest + +final class Q_municateUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // 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. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/Q-municate/Q-municateUITests/Q_municateUITestsLaunchTests.swift b/Q-municate/Q-municateUITests/Q_municateUITestsLaunchTests.swift new file mode 100644 index 000000000..525f4cbd2 --- /dev/null +++ b/Q-municate/Q-municateUITests/Q_municateUITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// Q_municateUITestsLaunchTests.swift +// Q-municateUITests +// +// Created by Injoit on 11.12.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import XCTest + +final class Q_municateUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/Screenshots/AudioCallScreens.png b/Screenshots/AudioCallScreens.png deleted file mode 100755 index b70c00eb1..000000000 Binary files a/Screenshots/AudioCallScreens.png and /dev/null differ diff --git a/Screenshots/ChatScreen.png b/Screenshots/ChatScreen.png deleted file mode 100755 index 4cc281036..000000000 Binary files a/Screenshots/ChatScreen.png and /dev/null differ diff --git a/Screenshots/ContactsListScreen.png b/Screenshots/ContactsListScreen.png deleted file mode 100755 index 6abce700f..000000000 Binary files a/Screenshots/ContactsListScreen.png and /dev/null differ diff --git a/Screenshots/ContactsSearch.png b/Screenshots/ContactsSearch.png deleted file mode 100755 index 5df880dc6..000000000 Binary files a/Screenshots/ContactsSearch.png and /dev/null differ diff --git a/Screenshots/DialogsListScreen.png b/Screenshots/DialogsListScreen.png deleted file mode 100755 index b73c8923d..000000000 Binary files a/Screenshots/DialogsListScreen.png and /dev/null differ diff --git a/Screenshots/EmailLoginScreen.png b/Screenshots/EmailLoginScreen.png deleted file mode 100755 index 4fe5742a8..000000000 Binary files a/Screenshots/EmailLoginScreen.png and /dev/null differ diff --git a/Screenshots/FeedbackScreen.png b/Screenshots/FeedbackScreen.png deleted file mode 100755 index 9407838b4..000000000 Binary files a/Screenshots/FeedbackScreen.png and /dev/null differ diff --git a/Screenshots/ForgotPasswordScreen.png b/Screenshots/ForgotPasswordScreen.png deleted file mode 100755 index 45a7a19b3..000000000 Binary files a/Screenshots/ForgotPasswordScreen.png and /dev/null differ diff --git a/Screenshots/GroupChatScreen.png b/Screenshots/GroupChatScreen.png deleted file mode 100755 index df4dffe5d..000000000 Binary files a/Screenshots/GroupChatScreen.png and /dev/null differ diff --git a/Screenshots/GroupInfoScreen.png b/Screenshots/GroupInfoScreen.png deleted file mode 100755 index 402448db3..000000000 Binary files a/Screenshots/GroupInfoScreen.png and /dev/null differ diff --git a/Screenshots/NewMessageScreen.png b/Screenshots/NewMessageScreen.png deleted file mode 100644 index 8bc846802..000000000 Binary files a/Screenshots/NewMessageScreen.png and /dev/null differ diff --git a/Screenshots/SettingsScreen.png b/Screenshots/SettingsScreen.png deleted file mode 100755 index b6439a9fb..000000000 Binary files a/Screenshots/SettingsScreen.png and /dev/null differ diff --git a/Screenshots/SettingsUpdateScreen.png b/Screenshots/SettingsUpdateScreen.png deleted file mode 100755 index 1e02d5493..000000000 Binary files a/Screenshots/SettingsUpdateScreen.png and /dev/null differ diff --git a/Screenshots/UserInfoScreen.png b/Screenshots/UserInfoScreen.png deleted file mode 100755 index 4e08d2ec9..000000000 Binary files a/Screenshots/UserInfoScreen.png and /dev/null differ diff --git a/Screenshots/VideCallScreens.png b/Screenshots/VideCallScreens.png deleted file mode 100755 index 07b97ec21..000000000 Binary files a/Screenshots/VideCallScreens.png and /dev/null differ diff --git a/Screenshots/WelcomeScreen.png b/Screenshots/WelcomeScreen.png deleted file mode 100755 index 8e832394a..000000000 Binary files a/Screenshots/WelcomeScreen.png and /dev/null differ