├── .gitignore ├── Automation └── Bitrise.io_configs │ └── app-customers_master_BuildAndDeploy.yml ├── Customers.UITests ├── AppInitializer.cs ├── Customers.UITests.csproj ├── Tests │ ├── AndroidTests.cs │ └── iOSTests.cs └── packages.config ├── Customers.sln ├── Customers ├── App.xaml ├── App.xaml.cs ├── Constants │ └── MessageKeys.cs ├── Converters │ └── BooleanInverter.cs ├── Customers.csproj ├── Data │ ├── Customer.cs │ ├── CustomerDataSource.cs │ ├── IDataSource.cs │ └── ILocalization.cs ├── Extensions │ ├── IntExtensions.cs │ └── StringExtensions.cs ├── Pages │ ├── CustomerDetailPage.xaml │ ├── CustomerDetailPage.xaml.cs │ ├── CustomerEditPage.xaml │ ├── CustomerEditPage.xaml.cs │ ├── CustomerListPage.xaml │ └── CustomerListPage.xaml.cs ├── Properties │ └── AssemblyInfo.cs ├── Services │ ├── CapabilityService.cs │ ├── ICapabilityService.cs │ └── IEnvironmentService.cs ├── ViewModels │ ├── BaseNavigationViewModel.cs │ ├── CustomerDetailViewModel.cs │ └── CustomerListViewModel.cs ├── Views │ └── FloatingActionButtonView.cs └── packages.config ├── Droid ├── Assets │ └── AboutAssets.txt ├── Customers.Droid.csproj ├── Localization.cs ├── MainActivity.cs ├── Properties │ ├── AndroidManifest.xml │ └── AssemblyInfo.cs ├── Renderers │ └── FloatingActionButtonViewRenderer.cs ├── Resources │ ├── AboutResources.txt │ ├── Resource.designer.cs │ ├── drawable-hdpi │ │ ├── directions.png │ │ ├── edit.png │ │ ├── email.png │ │ ├── fab_add.png │ │ ├── icon.png │ │ ├── message.png │ │ ├── phone.png │ │ ├── placeholderProfileImage.png │ │ └── save.png │ ├── drawable-ldpi │ │ ├── directions.png │ │ ├── edit.png │ │ ├── email.png │ │ ├── fab_add.png │ │ ├── icon.png │ │ ├── message.png │ │ ├── phone.png │ │ ├── placeholderProfileImage.png │ │ └── save.png │ ├── drawable-mdpi │ │ ├── directions.png │ │ ├── edit.png │ │ ├── email.png │ │ ├── fab_add.png │ │ ├── icon.png │ │ ├── message.png │ │ ├── phone.png │ │ ├── placeholderProfileImage.png │ │ └── save.png │ ├── drawable-xhdpi │ │ ├── directions.png │ │ ├── edit.png │ │ ├── email.png │ │ ├── fab_add.png │ │ ├── icon.png │ │ ├── message.png │ │ ├── phone.png │ │ ├── placeholderProfileImage.png │ │ └── save.png │ ├── drawable-xxhdpi │ │ ├── directions.png │ │ ├── edit.png │ │ ├── email.png │ │ ├── fab_add.png │ │ ├── icon.png │ │ ├── message.png │ │ ├── phone.png │ │ ├── placeholderProfileImage.png │ │ └── save.png │ ├── drawable-xxxhdpi │ │ ├── directions.png │ │ ├── edit.png │ │ ├── email.png │ │ ├── fab_add.png │ │ ├── icon.png │ │ ├── message.png │ │ ├── phone.png │ │ ├── placeholderProfileImage.png │ │ └── save.png │ ├── drawable │ │ ├── directions.png │ │ ├── edit.png │ │ ├── email.png │ │ ├── fab_add.png │ │ ├── icon.png │ │ ├── message.png │ │ ├── phone.png │ │ ├── placeholderProfileImage.png │ │ └── save.png │ ├── layout │ │ └── toolbar.axml │ ├── values-v21 │ │ └── styles.xml │ └── values │ │ ├── colors.xml │ │ └── styles.xml ├── Services │ └── EnvironmentService.cs ├── googleMapsApiKeyUtil.sh └── packages.config ├── LICENSE ├── README.md ├── Screenshots ├── Customers_DetailPage.png ├── Customers_EditPage.png ├── Customers_GetDirections.png ├── Customers_ListPage.png └── Customers_Screens.jpg └── iOS ├── AppDelegate.cs ├── Customers.iOS.csproj ├── Entitlements.plist ├── Info.plist ├── Localization.cs ├── Main.cs ├── Renderers └── StandardViewCellRenderer.cs ├── Resources ├── Images.xcassets │ └── AppIcons.appiconset │ │ └── Contents.json ├── LaunchScreen.xib ├── add.png ├── add@2x.png ├── add@3x.png ├── directions.png ├── directions@2x.png ├── directions@3x.png ├── edit.png ├── edit@2x.png ├── edit@3x.png ├── email.png ├── email@2x.png ├── email@3x.png ├── message.png ├── message@2x.png ├── message@3x.png ├── phone.png ├── phone@2x.png ├── phone@3x.png ├── placeholderProfileImage.png ├── save.png ├── save@2x.png └── save@3x.png ├── Services └── EnvironmentService.cs └── packages.config /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | *.userprefs 9 | 10 | # Xamarin Components 11 | Components/ 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | x64/ 18 | build/ 19 | bld/ 20 | [Bb]in/ 21 | [Oo]bj/ 22 | [Pp]ackages/ 23 | 24 | # MSTest test Results 25 | [Tt]est[Rr]esult*/ 26 | [Bb]uild[Ll]og.* 27 | 28 | #NUNIT 29 | *.VisualState.xml 30 | TestResult.xml 31 | 32 | # Build Results of an ATL Project 33 | [Dd]ebugPS/ 34 | [Rr]eleasePS/ 35 | dlldata.c 36 | 37 | *_i.c 38 | *_p.c 39 | *_i.h 40 | *.ilk 41 | *.meta 42 | *.obj 43 | *.pch 44 | *.pdb 45 | *.pgc 46 | *.pgd 47 | *.rsp 48 | *.sbr 49 | *.tlb 50 | *.tli 51 | *.tlh 52 | *.tmp 53 | *.tmp_proj 54 | *.log 55 | *.vspscc 56 | *.vssscc 57 | .builds 58 | *.pidb 59 | *.svclog 60 | *.scc 61 | 62 | # Chutzpah Test files 63 | _Chutzpah* 64 | 65 | # Visual C++ cache files 66 | ipch/ 67 | *.aps 68 | *.ncb 69 | *.opensdf 70 | *.sdf 71 | *.cachefile 72 | 73 | # Visual Studio profiler 74 | *.psess 75 | *.vsp 76 | *.vspx 77 | 78 | # TFS 2012 Local Workspace 79 | $tf/ 80 | 81 | # Guidance Automation Toolkit 82 | *.gpState 83 | 84 | # ReSharper is a .NET coding add-in 85 | _ReSharper*/ 86 | *.[Rr]e[Ss]harper 87 | *.DotSettings.user 88 | 89 | # JustCode is a .NET coding addin-in 90 | .JustCode 91 | 92 | # TeamCity is a build add-in 93 | _TeamCity* 94 | 95 | # DotCover is a Code Coverage Tool 96 | *.dotCover 97 | 98 | # NCrunch 99 | *.ncrunch* 100 | _NCrunch_* 101 | .*crunch*.local.xml 102 | 103 | # MightyMoose 104 | *.mm.* 105 | AutoTest.Net/ 106 | 107 | # Web workbench (sass) 108 | .sass-cache/ 109 | 110 | # Installshield output folder 111 | [Ee]xpress/ 112 | 113 | # DocProject is a documentation generator add-in 114 | DocProject/buildhelp/ 115 | DocProject/Help/*.HxT 116 | DocProject/Help/*.HxC 117 | DocProject/Help/*.hhc 118 | DocProject/Help/*.hhk 119 | DocProject/Help/*.hhp 120 | DocProject/Help/Html2 121 | DocProject/Help/html 122 | 123 | # Click-Once directory 124 | publish/ 125 | 126 | # Publish Web Output 127 | *.[Pp]ublish.xml 128 | *.azurePubxml 129 | 130 | # Windows Azure Build Output 131 | csx/ 132 | *.build.csdef 133 | 134 | # Windows Store app package directory 135 | AppPackages/ 136 | 137 | # Others 138 | sql/ 139 | *.Cache 140 | ClientBin/ 141 | [Ss]tyle[Cc]op.* 142 | ~$* 143 | *~ 144 | *.dbmdl 145 | *.dbproj.schemaview 146 | *.pfx 147 | *.publishsettings 148 | node_modules/ 149 | .DS_Store 150 | 151 | # RIA/Silverlight projects 152 | Generated_Code/ 153 | 154 | # Backup & report files from converting an old project file to a newer 155 | # Visual Studio version. Backup files are not needed, because we have git ;-) 156 | _UpgradeReport_Files/ 157 | Backup*/ 158 | UpgradeLog*.XML 159 | UpgradeLog*.htm 160 | 161 | # SQL Server files 162 | *.mdf 163 | *.ldf 164 | 165 | # Business Intelligence projects 166 | *.rdl.data 167 | *.bim.layout 168 | *.bim_*.settings 169 | 170 | # Microsoft Fakes 171 | FakesAssemblies/ 172 | 173 | *.bak -------------------------------------------------------------------------------- /Automation/Bitrise.io_configs/app-customers_master_BuildAndDeploy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | format_version: 1.1.0 3 | default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git 4 | app: 5 | envs: 6 | - opts: 7 | is_expand: true 8 | XAMARIN_SOLUTION_FILEPATH: Customers.sln 9 | - opts: 10 | is_expand: true 11 | XAMARIN_PROJECT_FOLDERPATH_IOS: iOS 12 | - opts: 13 | is_expand: true 14 | XAMARIN_PROJECT_FOLDERPATH_ANDROID: Droid 15 | - opts: 16 | is_expand: true 17 | XAMARIN_PROJECT_FILENAME_IOS: Customers.iOS.csproj 18 | - opts: 19 | is_expand: true 20 | XAMARIN_PROJECT_FILENAME_ANDROID: Customers.Droid.csproj 21 | - opts: 22 | is_expand: true 23 | XAMARIN_CONFIG_IOS: Release 24 | - opts: 25 | is_expand: true 26 | XAMARIN_CONFIG_ANDROID: Release 27 | - opts: 28 | is_expand: true 29 | XAMARIN_PLATFORM_IOS: iPhone 30 | - opts: 31 | is_expand: true 32 | XAMARIN_PLATFORM_ANDROID: AnyCPU 33 | - opts: 34 | is_expand: true 35 | XAMARIN_OUTPUT_FILENAME_IOS: Customers.ipa 36 | - opts: 37 | is_expand: true 38 | XAMARIN_OUTPUT_DSYM_UNZIPPED_FILENAME: CustomersiOS.app.dSYM 39 | - opts: 40 | is_expand: true 41 | XAMARIN_OUTPUT_DSYM_ZIPPED_FILENAME: CustomersiOS.app.dSYM.zip 42 | - opts: 43 | is_expand: true 44 | XAMARIN_OUTPUT_FILENAME_ANDROID_UNSIGNED: com.xamarin.customers.apk 45 | - opts: 46 | is_expand: true 47 | XAMARIN_OUTPUT_FILENAME_ANDROID_SIGNED: signed-com.xamarin.customers.apk 48 | - opts: 49 | is_expand: true 50 | XAMARIN_IOS_INFOPLIST_FILEPATH: "$XAMARIN_PROJECT_FOLDERPATH_IOS/Info.plist" 51 | - opts: 52 | is_expand: true 53 | XAMARIN_ANDROID_MANIFESTXML_FILEPATH: "$XAMARIN_PROJECT_FOLDERPATH_ANDROID/Properties/AndroidManifest.xml" 54 | - opts: 55 | is_expand: true 56 | XAMARIN_PROJECT_FILEPATH_IOS: "$XAMARIN_PROJECT_FOLDERPATH_IOS/$XAMARIN_PROJECT_FILENAME_IOS" 57 | - opts: 58 | is_expand: true 59 | XAMARIN_PROJECT_FILEPATH_ANDROID: "$XAMARIN_PROJECT_FOLDERPATH_ANDROID/$XAMARIN_PROJECT_FILENAME_ANDROID" 60 | - opts: 61 | is_expand: true 62 | XAMARIN_OUTPUT_FOLDERPATH_IOS: "$XAMARIN_PROJECT_FOLDERPATH_IOS/bin/$XAMARIN_PLATFORM_IOS/$XAMARIN_CONFIG_IOS" 63 | - opts: 64 | is_expand: true 65 | XAMARIN_OUTPUT_FOLDERPATH_ANDROID: "$XAMARIN_PROJECT_FOLDERPATH_ANDROID/bin/$XAMARIN_PLATFORM_ANDROID/$XAMARIN_CONFIG_ANDROID" 66 | - opts: 67 | is_expand: true 68 | XAMARIN_PROJECT_FILEPATH_ANDROID: "$XAMARIN_PROJECT_FOLDERPATH_ANDROID/$XAMARIN_PROJECT_FILENAME_ANDROID" 69 | - opts: 70 | is_expand: true 71 | XAMARIN_IPA_PATH: "$XAMARIN_OUTPUT_FOLDERPATH_IOS/$XAMARIN_OUTPUT_FILENAME_IOS" 72 | - opts: 73 | is_expand: true 74 | XAMARIN_APK_SOURCE_PATH: "$XAMARIN_OUTPUT_FOLDERPATH_ANDROID/$XAMARIN_OUTPUT_FILENAME_ANDROID_UNSIGNED" 75 | - opts: 76 | is_expand: true 77 | XAMARIN_APK_DESTINATION_PATH: "$XAMARIN_OUTPUT_FOLDERPATH_ANDROID/$XAMARIN_OUTPUT_FILENAME_ANDROID_SIGNED" 78 | - opts: 79 | is_expand: true 80 | XAMARIN_OUTPUT_DSYM_UNZIPPED_FILEPATH: "$XAMARIN_OUTPUT_FOLDERPATH_IOS/$XAMARIN_OUTPUT_DSYM_UNZIPPED_FILENAME" 81 | - opts: 82 | is_expand: true 83 | XAMARIN_OUTPUT_DSYM_ZIPPED_FILEPATH: "$XAMARIN_OUTPUT_FOLDERPATH_IOS/$XAMARIN_OUTPUT_DSYM_ZIPPED_FILENAME" 84 | trigger_map: 85 | - pattern: master 86 | is_pull_request_allowed: false 87 | workflow: master 88 | workflows: 89 | master: 90 | steps: 91 | - activate-ssh-key: 92 | title: Activate App SSH key 93 | inputs: 94 | - ssh_key_save_path: "$HOME/.ssh/steplib_ssh_step_id_rsa" 95 | - git-clone: {} 96 | - certificate-and-profile-installer: {} 97 | - xamarin-user-management: 98 | title: Xamarin Login 99 | - nuget-restore: 100 | title: Restore NuGets 101 | inputs: 102 | - xamarin_solution: "$XAMARIN_SOLUTION_FILEPATH" 103 | - xamarin-components-restore: 104 | title: Restore Components 105 | inputs: 106 | - xamarin_solution: "$XAMARIN_SOLUTION_FILEPATH" 107 | - set-xcode-build-number@1.0.1: 108 | inputs: 109 | - plist_path: "$XAMARIN_IOS_INFOPLIST_FILEPATH" 110 | - xamarin-builder@0.9.2: 111 | title: Build iOS 112 | inputs: 113 | - xamarin_project: "$XAMARIN_PROJECT_FILEPATH_IOS" 114 | - xamarin_configuration: "$XAMARIN_CONFIG_IOS" 115 | - xamarin_platform: "$XAMARIN_PLATFORM_IOS" 116 | - is_clean_build: 'true' 117 | - script@1.1.0: 118 | title: Upload DSYMs to Xamarin Insights 119 | inputs: 120 | - content: |- 121 | #!/bin/bash 122 | 123 | zip -r $XAMARIN_OUTPUT_DSYM_ZIPPED_FILEPATH $XAMARIN_OUTPUT_DSYM_UNZIPPED_FILEPATH 124 | 125 | curl -F "dsym=@$XAMARIN_OUTPUT_DSYM_ZIPPED_FILEPATH;type=application/zip" https://xaapi.xamarin.com/api/dsym?apikey=$XAMARIN_INSIGHTS_KEY 126 | opts: 127 | is_expand: true 128 | - script: 129 | title: Inject Google Maps API key 130 | inputs: 131 | - content: |- 132 | #!/bin/bash 133 | 134 | sed -i '' "s/GOOGLE_MAPS_API_KEY/$GOOGLE_MAPS_API_KEY/g" "$XAMARIN_PROJECT_FOLDERPATH_ANDROID/Properties/AndroidManifest.xml" 135 | opts: 136 | is_expand: true 137 | - working_dir: '' 138 | opts: 139 | is_expand: false 140 | - xamarin-builder: 141 | title: Build Android 142 | inputs: 143 | - xamarin_project: "$XAMARIN_PROJECT_FILEPATH_ANDROID" 144 | - xamarin_configuration: "$XAMARIN_CONFIG_ANDROID" 145 | - xamarin_platform: "$XAMARIN_PLATFORM_ANDROID" 146 | - is_clean_build: 'false' 147 | - command: archive 148 | - sign-apk: 149 | inputs: 150 | - apk_path: "$XAMARIN_APK_SOURCE_PATH" 151 | - xamarin-user-management: 152 | title: Xamarin Logout 153 | inputs: 154 | - xamarin_action: logout 155 | - deploy-to-bitrise-io: 156 | title: Deploy IPA 157 | inputs: 158 | - deploy_path: "$XAMARIN_IPA_PATH" 159 | - deploy-to-bitrise-io: 160 | title: Deploy APK 161 | inputs: 162 | - deploy_path: "$XAMARIN_APK_DESTINATION_PATH" 163 | before_run: 164 | after_run: 165 | -------------------------------------------------------------------------------- /Customers.UITests/AppInitializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using Xamarin.UITest; 5 | using Xamarin.UITest.Queries; 6 | 7 | namespace Customers.UITests 8 | { 9 | public class AppInitializer 10 | { 11 | public static IApp StartApp(Platform platform) 12 | { 13 | if (platform == Platform.Android) 14 | { 15 | return ConfigureApp 16 | .Android 17 | .ApkFile ("../../../Droid/bin/Release/com.xamarin.customers-Signed.apk") 18 | .StartApp(); 19 | } 20 | 21 | return ConfigureApp 22 | .iOS 23 | .AppBundle ("../../../iOS/bin/iPhoneSimulator/Debug/CustomersiOS.app") 24 | .StartApp(); 25 | } 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Customers.UITests/Customers.UITests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66} 7 | Library 8 | Customers.UITests 9 | Customers.UITests 10 | v4.5 11 | 12 | 13 | true 14 | full 15 | false 16 | bin\Debug 17 | DEBUG; 18 | prompt 19 | 4 20 | false 21 | 22 | 23 | full 24 | true 25 | bin\Release 26 | prompt 27 | 4 28 | false 29 | 30 | 31 | 32 | 33 | ..\packages\NUnit.2.6.3\lib\nunit.framework.dll 34 | 35 | 36 | ..\packages\Xamarin.UITest.1.2.0\lib\Xamarin.UITest.dll 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Customers.UITests/Tests/AndroidTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using NUnit.Framework; 5 | using Xamarin.UITest; 6 | using Xamarin.UITest.Queries; 7 | using System.Collections.Generic; 8 | 9 | namespace Customers.UITests 10 | { 11 | [TestFixture(Platform.Android)] 12 | public class AndroidTests 13 | { 14 | IApp app; 15 | Platform platform; 16 | 17 | public AndroidTests(Platform platform) 18 | { 19 | this.platform = platform; 20 | } 21 | 22 | [SetUp] 23 | public void BeforeEachTest() 24 | { 25 | app = AppInitializer.StartApp(platform); 26 | } 27 | 28 | 29 | [Test] 30 | public void EditCustomer () 31 | { 32 | app.Tap(x => x.Marked("Armstead, Evan")); 33 | app.Screenshot("Selected contact with name: 'Armstead, Evan'"); 34 | app.Tap(x => x.Marked("Edit")); 35 | app.Screenshot("Tapped on 'edit' button"); 36 | app.Tap(x => x.Marked("First").Sibling()); 37 | app.ClearText(x => x.Marked("First").Sibling()); 38 | app.EnterText("Edited"); 39 | app.Screenshot("Entered 'Edited' into First Name Field"); 40 | app.Tap(x => x.Marked("Last").Sibling()); 41 | app.ClearText(x => x.Marked("Last").Sibling()); 42 | app.EnterText("Name"); 43 | app.Screenshot("Entered 'Name' into Last Name Field"); 44 | app.Tap(x => x.Class("ActionMenuItemView")); 45 | app.Screenshot("Saved contact"); 46 | app.WaitForElement(x => x.Text("Edited Name")); 47 | app.Screenshot("Verified contacts name changed to: 'Edited Name'"); 48 | } 49 | 50 | [Test] 51 | public void EditCustomerInList () 52 | { 53 | app.Tap(x => x.Marked("Cardell, Jesus")); 54 | app.Screenshot("Selected contact with name: 'Cardell, Jesus'"); 55 | app.Tap(x => x.Marked("Edit")); 56 | app.Screenshot("Tapped on 'Edit' button"); 57 | app.Tap(x => x.Marked("First").Sibling()); 58 | app.ClearText(x => x.Marked("First").Sibling()); 59 | app.EnterText("New Name"); 60 | app.Screenshot("Entered 'New Name' into First Name Field"); 61 | app.Tap(x => x.Marked("Last").Sibling()); 62 | app.ClearText(x => x.Marked("Last").Sibling()); 63 | app.EnterText("In List"); 64 | app.Screenshot("Entered 'In List' into Last Name Field"); 65 | app.Tap(x => x.Class("ActionMenuItemView")); 66 | app.Screenshot("Saved contact"); 67 | app.Tap(x => x.Class("android.widget.ImageButton")); 68 | app.Screenshot("Tapped on 'back' button"); 69 | app.ScrollDownTo(x => x.Marked("In List, New Name")); 70 | app.Screenshot("Verified edited name appears in list"); 71 | } 72 | 73 | [Test] 74 | public void UpdateCustomerAddress () 75 | { 76 | app.Tap(x => x.Marked("Boone, John")); 77 | app.Screenshot("Selected contact with name: 'Boone, John'"); 78 | app.WaitForElement("Google Map"); 79 | app.Screenshot("Verified map is set to current address"); 80 | app.Tap(x => x.Marked("Edit")); 81 | app.Screenshot("Tapped on 'Edit' button"); 82 | app.ScrollDownTo(x => x.Marked("Street").Sibling()); 83 | app.Tap(x => x.Marked("Street").Sibling()); 84 | app.ClearText(x => x.Marked("Street").Sibling()); 85 | app.EnterText("394 Pacific Ave"); 86 | app.Screenshot("Entered '394 Pacific Ave' into Street Field"); 87 | app.PressEnter(); 88 | app.ScrollDownTo("City"); 89 | app.Tap(x => x.Marked("City").Sibling()); 90 | app.ClearText(x => x.Marked("City").Sibling()); 91 | app.EnterText("San Francisco"); 92 | app.Screenshot("Entered 'San Francisco' into City Field"); 93 | app.PressEnter(); 94 | app.ScrollDownTo(x => x.Marked("State").Sibling()); 95 | app.Tap(x => x.Marked("State").Sibling()); 96 | app.ClearText(x => x.Marked("State").Sibling()); 97 | app.EnterText("CA"); 98 | app.Screenshot("Entered 'CA' into State Field"); 99 | app.PressEnter(); 100 | app.Tap(x => x.Marked("ZIP").Sibling()); 101 | app.ClearText(x => x.Marked("ZIP").Sibling()); 102 | app.EnterText("94111"); 103 | app.Screenshot("Entered '94111' into ZIP Field"); 104 | app.DismissKeyboard(); 105 | app.Tap(x => x.Class("ActionMenuItemView")); 106 | app.Screenshot("Saved contact"); 107 | app.WaitForElement("Google Map"); 108 | app.Screenshot("Verified map is set to new address"); 109 | } 110 | 111 | [Test] 112 | public void AddNewCustomer () 113 | { 114 | app.Tap(x => x.Class("FloatingActionButton")); 115 | app.Screenshot("Tapped on 'add' button"); 116 | app.Tap(x => x.Marked("First").Sibling()); 117 | app.EnterText("NEW"); 118 | app.Screenshot("Entered 'NEW' into First Name Field"); 119 | app.DismissKeyboard(); 120 | app.ScrollDownTo(x => x.Marked("Last").Sibling()); 121 | app.Tap(x => x.Marked("Last").Sibling()); 122 | app.EnterText("CONTACT"); 123 | app.Screenshot("Entered 'CONTACT' into Last Name Field"); 124 | app.DismissKeyboard(); 125 | app.ScrollDownTo(x => x.Marked("Company").Sibling()); 126 | app.Tap(x => x.Marked("Company").Sibling()); 127 | app.EnterText("Xamarin"); 128 | app.Screenshot("Entered 'Xamarin' into Company Field"); 129 | app.DismissKeyboard(); 130 | app.ScrollDownTo(x => x.Marked("Title").Sibling()); 131 | app.Tap(x => x.Marked("Title").Sibling()); 132 | app.EnterText("Test Cloud"); 133 | app.Screenshot("Entered 'Test Cloud' into Title Field"); 134 | app.DismissKeyboard(); 135 | app.ScrollDownTo(x => x.Marked("Phone").Sibling()); 136 | app.Tap(x => x.Marked("Phone").Sibling()); 137 | app.EnterText("1234567890"); 138 | app.Screenshot("Entered '1234567890' into Phone Field"); 139 | app.DismissKeyboard(); 140 | app.ScrollDownTo(x => x.Marked("Email").Sibling()); 141 | app.Tap(x => x.Marked("Email").Sibling()); 142 | app.EnterText("hello@xamarin.com"); 143 | app.Screenshot("Entered 'hello@xamarin.com' into Email Field"); 144 | app.DismissKeyboard(); 145 | app.ScrollDownTo(x => x.Marked("Street").Sibling()); 146 | app.Tap(x => x.Marked("Street").Sibling()); 147 | app.EnterText("394 Pacific Ave"); 148 | app.Screenshot("Entered '394 Pacific Ave' into Street Field"); 149 | app.DismissKeyboard(); 150 | app.ScrollDownTo(x => x.Marked("City").Sibling()); 151 | app.Tap(x => x.Marked("City").Sibling()); 152 | app.EnterText("San Francisco"); 153 | app.Screenshot("Entered 'San Francisco' into City Field"); 154 | app.DismissKeyboard(); 155 | app.ScrollDownTo(x => x.Marked("State").Sibling()); 156 | app.Tap(x => x.Marked("State").Sibling()); 157 | app.EnterText("CA"); 158 | app.Screenshot("Entered 'CA' into State Field"); 159 | app.DismissKeyboard(); 160 | app.ScrollDownTo(x => x.Marked("ZIP").Sibling()); 161 | app.Tap(x => x.Marked("ZIP").Sibling()); 162 | app.EnterText("94111"); 163 | app.Screenshot("Entered '94111' into City Field"); 164 | app.DismissKeyboard(); 165 | app.Tap(x => x.Class("ActionMenuItemView")); 166 | app.Screenshot("Saved contact"); 167 | app.WaitForElement("NEW CONTACT", timeout:TimeSpan.FromSeconds(10)); 168 | app.Screenshot("Verified on NEW CONTACT's Details Page"); 169 | } 170 | 171 | [TestCase("message")] 172 | [TestCase("phone")] 173 | [TestCase("email")] 174 | [TestCase("directions")] 175 | public void VerifyExternalLink (string link) 176 | { 177 | Dictionary LinkIndexes = new Dictionary(); 178 | LinkIndexes.Add("message", 2); 179 | LinkIndexes.Add("phone", 3); 180 | LinkIndexes.Add("email", 4); 181 | LinkIndexes.Add("directions", 1); 182 | 183 | app.Tap(x => x.Marked("Bell, Floyd")); 184 | app.Screenshot("Selected contact with name: 'Bell, Floyd'"); 185 | 186 | app.Tap(x => x.Class("FormsImageView").Index(LinkIndexes[link])); 187 | System.Threading.Thread.Sleep(5000); 188 | app.Screenshot(String.Format("Verify {0} opened", link)); 189 | } 190 | } 191 | } 192 | 193 | -------------------------------------------------------------------------------- /Customers.UITests/Tests/iOSTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using NUnit.Framework; 5 | using Xamarin.UITest; 6 | using Xamarin.UITest.Queries; 7 | 8 | namespace Customers.UITests 9 | { 10 | [TestFixture(Platform.iOS)] 11 | public class iOSTests 12 | { 13 | IApp app; 14 | Platform platform; 15 | 16 | public iOSTests(Platform platform) 17 | { 18 | this.platform = platform; 19 | } 20 | 21 | [SetUp] 22 | public void BeforeEachTest() 23 | { 24 | app = AppInitializer.StartApp(platform); 25 | } 26 | 27 | [Test] 28 | public void EditCustomer () 29 | { 30 | app.Tap(x => x.Marked("Armstead, Evan")); 31 | app.Screenshot("Selected contact with name: 'Armstead, Evan'"); 32 | app.Tap(x => x.Marked("edit")); 33 | app.Screenshot("Tapped on 'edit' button"); 34 | app.Tap(x => x.Marked("First").Sibling()); 35 | app.ClearText(x => x.Marked("First").Sibling()); 36 | app.EnterText("Edited"); 37 | app.Screenshot("Entered 'Edited' into First Name Field"); 38 | app.Tap(x => x.Marked("Last").Sibling()); 39 | app.ClearText(x => x.Marked("Last").Sibling()); 40 | app.EnterText("Name"); 41 | app.Screenshot("Entered 'Name' into Last Name Field"); 42 | app.Tap(x => x.Id("save")); 43 | app.Screenshot("Saved contact"); 44 | app.WaitForElement(x => x.Text("Edited Name")); 45 | app.Screenshot("Verified contacts name changed to: 'Edited Name'"); 46 | } 47 | 48 | [Test] 49 | public void EditCustomerInList () 50 | { 51 | app.Tap(x => x.Marked("Cardell, Jesus")); 52 | app.Screenshot("Selected contact with name: 'Cardell, Jesus'"); 53 | app.Tap(x => x.Marked("edit")); 54 | app.Screenshot("Tapped on 'edit' button"); 55 | app.Tap(x => x.Marked("First").Sibling()); 56 | app.ClearText(x => x.Marked("First").Sibling()); 57 | app.EnterText("New Name"); 58 | app.Screenshot("Entered 'New Name' into First Name Field"); 59 | app.Tap(x => x.Marked("Last").Sibling()); 60 | app.ClearText(x => x.Marked("Last").Sibling()); 61 | app.EnterText("In List"); 62 | app.Screenshot("Entered 'In List' into Last Name Field"); 63 | app.Tap(x => x.Id("save")); 64 | app.Screenshot("Saved contact"); 65 | app.Tap(x => x.Marked("Back")); 66 | app.Screenshot("Tapped on 'back' button"); 67 | app.ScrollDownTo(x => x.Marked("In List, New Name")); 68 | app.Screenshot("Verified edited name appears in list"); 69 | } 70 | 71 | [Test] 72 | public void UpdateCustomerAddress () 73 | { 74 | app.Tap(x => x.Marked("Boone, John")); 75 | app.Screenshot("Selected contact with name: 'Boone, John'"); 76 | app.WaitForElement(x => x.Class("MKNewAnnotationContainerView")); 77 | app.Screenshot("Verified map is set to current address"); 78 | app.Tap(x => x.Marked("edit")); 79 | app.Screenshot("Tapped on 'edit' button"); 80 | app.ScrollDownTo(x => x.Marked("Street").Sibling()); 81 | app.Tap(x => x.Marked("Street").Sibling()); 82 | app.ClearText(x => x.Marked("Street").Sibling()); 83 | app.EnterText("394 Pacific Ave"); 84 | app.Screenshot("Entered '394 Pacific Ave' into Street Field"); 85 | app.PressEnter(); 86 | app.ScrollDownTo("City"); 87 | app.Tap(x => x.Marked("City").Sibling()); 88 | app.ClearText(x => x.Marked("City").Sibling()); 89 | app.EnterText("San Francisco"); 90 | app.Screenshot("Entered 'San Francisco' into City Field"); 91 | app.PressEnter(); 92 | app.Tap(x => x.Marked("State").Sibling()); 93 | app.ClearText(x => x.Marked("State").Sibling()); 94 | app.EnterText("CA"); 95 | app.Screenshot("Entered 'CA' into State Field"); 96 | app.PressEnter(); 97 | app.Tap(x => x.Marked("ZIP").Sibling()); 98 | app.ClearText(x => x.Marked("ZIP").Sibling()); 99 | app.EnterText("94111"); 100 | app.Screenshot("Entered '94111' into ZIP Field"); 101 | app.Tap(x => x.Id("save")); 102 | app.Screenshot("Saved contact"); 103 | app.WaitForElement(x => x.Class("MKNewAnnotationContainerView")); 104 | app.Screenshot("Verified map is set to new address"); 105 | } 106 | 107 | [Test] 108 | public void AddNewCustomer () 109 | { 110 | app.Tap(x => x.Marked("add")); 111 | app.Screenshot("Tapped on 'add' button"); 112 | app.Tap(x => x.Marked("First").Sibling()); 113 | app.EnterText("NEW"); 114 | app.Screenshot("Entered 'NEW' into First Name Field"); 115 | app.PressEnter(); 116 | app.Tap(x => x.Marked("Last").Sibling()); 117 | app.EnterText("CONTACT"); 118 | app.Screenshot("Entered 'CONTACT' into Last Name Field"); 119 | app.PressEnter(); 120 | app.Tap(x => x.Marked("Company").Sibling()); 121 | app.EnterText("Xamarin"); 122 | app.Screenshot("Entered 'Xamarin' into Company Field"); 123 | app.PressEnter(); 124 | app.Tap(x => x.Marked("Title").Sibling()); 125 | app.EnterText("Test Cloud"); 126 | app.Screenshot("Entered 'Test Cloud' into Title Field"); 127 | app.PressEnter(); 128 | app.Tap(x => x.Marked("Phone").Sibling()); 129 | app.EnterText("1234567890"); 130 | app.Screenshot("Entered '1234567890' into Phone Field"); 131 | app.DismissKeyboard(); 132 | app.Tap(x => x.Marked("Email").Sibling()); 133 | app.EnterText("hello@xamarin.com"); 134 | app.Screenshot("Entered 'hello@xamarin.com' into Email Field"); 135 | app.PressEnter(); 136 | app.Tap(x => x.Marked("Street").Sibling()); 137 | app.EnterText("394 Pacific Ave"); 138 | app.Screenshot("Entered '394 Pacific Ave' into Street Field"); 139 | app.PressEnter(); 140 | app.Tap(x => x.Marked("City").Sibling()); 141 | app.EnterText("San Francisco"); 142 | app.Screenshot("Entered 'San Francisco' into City Field"); 143 | app.PressEnter(); 144 | app.Tap(x => x.Marked("State").Sibling()); 145 | app.EnterText("CA"); 146 | app.Screenshot("Entered 'CA' into State Field"); 147 | app.PressEnter(); 148 | app.Tap(x => x.Marked("ZIP").Sibling()); 149 | app.EnterText("94111"); 150 | app.Screenshot("Entered '94111' into City Field"); 151 | app.DismissKeyboard(); 152 | app.Tap(x => x.Id("save")); 153 | app.Screenshot("Saved contact"); 154 | app.WaitForElement("NEW CONTACT", timeout:TimeSpan.FromSeconds(10)); 155 | app.Screenshot("Verified on NEW CONTACT's Details Page"); 156 | } 157 | 158 | [TestCase("message")] 159 | [TestCase("phone")] 160 | [TestCase("email")] 161 | [TestCase("directions")] 162 | public void VerifyExternalLink (string link) 163 | { 164 | app.Tap(x => x.Marked("Bell, Floyd")); 165 | app.Screenshot("Selected contact with name: 'Bell, Floyd'"); 166 | 167 | app.Tap(link); 168 | System.Threading.Thread.Sleep(5000); 169 | app.Screenshot(String.Format("Verify {0} opened", link)); 170 | } 171 | } 172 | } 173 | 174 | -------------------------------------------------------------------------------- /Customers.UITests/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Customers.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | VisualStudioVersion = 14.0.24720.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Customers", "Customers\Customers.csproj", "{55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Customers.iOS", "iOS\Customers.iOS.csproj", "{38CAF316-C475-4099-9723-A4002F76FCD2}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Customers.Droid", "Droid\Customers.Droid.csproj", "{186E2C95-63F1-44B9-AE99-0F436E3DADFE}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Customers.UITests", "Customers.UITests\Customers.UITests.csproj", "{02FC1245-00EF-4946-9C94-1CCA2CF51B66}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Debug|iPhone = Debug|iPhone 18 | Debug|iPhoneSimulator = Debug|iPhoneSimulator 19 | Debug|x86 = Debug|x86 20 | Release|Any CPU = Release|Any CPU 21 | Release|iPhone = Release|iPhone 22 | Release|iPhoneSimulator = Release|iPhoneSimulator 23 | Release|x86 = Release|x86 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Debug|iPhone.ActiveCfg = Debug|Any CPU 29 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Debug|iPhone.Build.0 = Debug|Any CPU 30 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 31 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 32 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Debug|x86.ActiveCfg = Debug|Any CPU 33 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Debug|x86.Build.0 = Debug|Any CPU 34 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Release|iPhone.ActiveCfg = Release|Any CPU 37 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Release|iPhone.Build.0 = Release|Any CPU 38 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 39 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 40 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Release|x86.ActiveCfg = Release|Any CPU 41 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Release|x86.Build.0 = Release|Any CPU 42 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Debug|Any CPU.Deploy.0 = Debug|Any CPU 45 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Debug|iPhone.ActiveCfg = Debug|Any CPU 46 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Debug|iPhone.Build.0 = Debug|Any CPU 47 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 48 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 49 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Debug|x86.ActiveCfg = Debug|Any CPU 50 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Debug|x86.Build.0 = Debug|Any CPU 51 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Release|iPhone.ActiveCfg = Release|Any CPU 54 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Release|iPhone.Build.0 = Release|Any CPU 55 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 56 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 57 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Release|x86.ActiveCfg = Release|Any CPU 58 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Release|x86.Build.0 = Release|Any CPU 59 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator 60 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Debug|Any CPU.Build.0 = Debug|iPhoneSimulator 61 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Debug|iPhone.ActiveCfg = Debug|iPhone 62 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Debug|iPhone.Build.0 = Debug|iPhone 63 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator 64 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator 65 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Debug|x86.ActiveCfg = Debug|iPhoneSimulator 66 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Debug|x86.Build.0 = Debug|iPhoneSimulator 67 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Release|Any CPU.ActiveCfg = Release|iPhone 68 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Release|Any CPU.Build.0 = Release|iPhone 69 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Release|iPhone.ActiveCfg = Release|iPhone 70 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Release|iPhone.Build.0 = Release|iPhone 71 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator 72 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator 73 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Release|x86.ActiveCfg = Release|iPhone 74 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Release|x86.Build.0 = Release|iPhone 75 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 76 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Debug|Any CPU.Build.0 = Debug|Any CPU 77 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Debug|iPhone.ActiveCfg = Debug|Any CPU 78 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Debug|iPhone.Build.0 = Debug|Any CPU 79 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 80 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 81 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Debug|x86.ActiveCfg = Debug|Any CPU 82 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Debug|x86.Build.0 = Debug|Any CPU 83 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Release|Any CPU.ActiveCfg = Release|Any CPU 84 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Release|Any CPU.Build.0 = Release|Any CPU 85 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Release|iPhone.ActiveCfg = Release|Any CPU 86 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Release|iPhone.Build.0 = Release|Any CPU 87 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 88 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 89 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Release|x86.ActiveCfg = Release|Any CPU 90 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Release|x86.Build.0 = Release|Any CPU 91 | EndGlobalSection 92 | GlobalSection(SolutionProperties) = preSolution 93 | HideSolutionNode = FALSE 94 | EndGlobalSection 95 | EndGlobal 96 | -------------------------------------------------------------------------------- /Customers/App.xaml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Customers/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using Xamarin.Forms; 2 | using FormsToolkit; 3 | 4 | namespace Customers 5 | { 6 | public partial class App : Application 7 | { 8 | public App() 9 | { 10 | InitializeComponent(); 11 | 12 | SubscribeToDisplayAlertMessages(); 13 | 14 | if (Device.OS == TargetPlatform.iOS) 15 | navPage.BarTextColor = Color.White; 16 | } 17 | 18 | /// 19 | /// Subscribes to messages for displaying alerts. 20 | /// 21 | static void SubscribeToDisplayAlertMessages() 22 | { 23 | MessagingService.Current.Subscribe(MessageKeys.DisplayAlert, async (service, info) => 24 | { 25 | var task = Application.Current?.MainPage?.DisplayAlert(info.Title, info.Message, info.Cancel); 26 | if (task != null) 27 | { 28 | await task; 29 | info?.OnCompleted?.Invoke(); 30 | } 31 | }); 32 | 33 | MessagingService.Current.Subscribe(MessageKeys.DisplayQuestion, async (service, info) => 34 | { 35 | var task = Application.Current?.MainPage?.DisplayAlert(info.Title, info.Question, info.Positive, info.Negative); 36 | if (task != null) 37 | { 38 | var result = await task; 39 | info?.OnCompleted?.Invoke(result); 40 | } 41 | }); 42 | 43 | MessagingService.Current.Subscribe(MessageKeys.DisplayGeocodingError, async (service) => 44 | { 45 | var task = Application.Current?.MainPage?.DisplayAlert("Geocoding Error", "An eror occurred while converting the street address to GPS coordinates.", "OK"); 46 | if (task != null) 47 | await task; 48 | }); 49 | } 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Customers/Constants/MessageKeys.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Customers 4 | { 5 | public static class MessageKeys 6 | { 7 | public const string DisplayAlert = "DisplayAlert"; 8 | public const string DisplayQuestion = "DisplayQuestion"; 9 | public const string SaveCustomer = "SaveCustomer"; 10 | public const string DeleteCustomer = "DeleteCustomer"; 11 | public const string CustomerLocationUpdated = "CustomerLocationUpdated"; 12 | public const string SetupMap = "SetupMap"; 13 | public const string DisplayGeocodingError = "DisplayGeocodingError"; 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /Customers/Converters/BooleanInverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xamarin.Forms; 3 | using System.Globalization; 4 | 5 | namespace Customers 6 | { 7 | public class BooleanInverter : IValueConverter 8 | { 9 | #region IValueConverter implementation 10 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 11 | { 12 | return !(bool)value; 13 | } 14 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 15 | { 16 | return !(bool)value; 17 | } 18 | #endregion 19 | 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /Customers/Customers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 7 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0} 8 | Library 9 | Customers 10 | Customers 11 | v4.5 12 | Profile78 13 | 14 | 15 | true 16 | full 17 | false 18 | bin\Debug 19 | DEBUG; 20 | prompt 21 | 4 22 | false 23 | 24 | 25 | full 26 | true 27 | bin\Release 28 | prompt 29 | 4 30 | false 31 | 32 | 33 | 34 | 35 | App.xaml 36 | 37 | 38 | CustomerListPage.xaml 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | CustomerEditPage.xaml 49 | 50 | 51 | CustomerDetailPage.xaml 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | ..\packages\Xamarin.Forms.2.0.0.6490\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\Xamarin.Forms.Core.dll 69 | 70 | 71 | ..\packages\Xamarin.Forms.2.0.0.6490\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\Xamarin.Forms.Xaml.dll 72 | 73 | 74 | ..\packages\Xamarin.Forms.2.0.0.6490\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\Xamarin.Forms.Platform.dll 75 | 76 | 77 | ..\packages\Microsoft.Net.Http.2.2.29\lib\portable-net40+sl4+win8+wp71+wpa81\System.Net.Http.dll 78 | 79 | 80 | ..\packages\Microsoft.Net.Http.2.2.29\lib\portable-net40+sl4+win8+wp71+wpa81\System.Net.Http.Primitives.dll 81 | 82 | 83 | ..\packages\Microsoft.Net.Http.2.2.29\lib\portable-net40+sl4+win8+wp71+wpa81\System.Net.Http.Extensions.dll 84 | 85 | 86 | ..\packages\Microsoft.Azure.Mobile.Client.2.0.1\lib\portable-win+net45+wp8+wpa81+monotouch+monoandroid\Microsoft.WindowsAzure.Mobile.dll 87 | 88 | 89 | ..\packages\SQLitePCL.3.8.7.2\lib\portable-net45+sl50+win+wpa81+wp80+MonoAndroid10+xamarinios10+MonoTouch10\SQLitePCL.dll 90 | 91 | 92 | ..\packages\Microsoft.Azure.Mobile.Client.SQLiteStore.2.0.1\lib\portable-win+net45+wp8+wpa81+monotouch+monoandroid\Microsoft.WindowsAzure.Mobile.SQLiteStore.dll 93 | 94 | 95 | ..\packages\PCLStorage.1.0.2\lib\portable-net45+wp8+wpa81+win8+monoandroid+monotouch+Xamarin.iOS+Xamarin.Mac\PCLStorage.dll 96 | 97 | 98 | ..\packages\PCLStorage.1.0.2\lib\portable-net45+wp8+wpa81+win8+monoandroid+monotouch+Xamarin.iOS+Xamarin.Mac\PCLStorage.Abstractions.dll 99 | 100 | 101 | ..\packages\Xam.Plugins.Forms.ImageCircle.1.3.0\lib\portable-net45+wp8+win8+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10+UAP10\ImageCircle.Forms.Plugin.Abstractions.dll 102 | 103 | 104 | ..\packages\Xamarin.Forms.Maps.2.0.0.6490\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\Xamarin.Forms.Maps.dll 105 | 106 | 107 | ..\packages\Xam.Plugins.Messaging.3.0.0\lib\portable-net45+wp8+wpa81+win8+MonoAndroid10+MonoTouch10+Xamarin.iOS10+UAP10\Lotz.Xam.Messaging.Abstractions.dll 108 | 109 | 110 | ..\packages\Xam.Plugins.Messaging.3.0.0\lib\portable-net45+wp8+wpa81+win8+MonoAndroid10+MonoTouch10+Xamarin.iOS10+UAP10\Lotz.Xam.Messaging.dll 111 | 112 | 113 | ..\packages\Newtonsoft.Json.8.0.2\lib\portable-net45+wp80+win8+wpa81+dnxcore50\Newtonsoft.Json.dll 114 | 115 | 116 | ..\packages\Refractored.MvvmHelpers.1.0.1\lib\portable-net45+wp8+wpa81+win8+MonoAndroid10+MonoTouch10+Xamarin.iOS10+UAP10\MvvmHelpers.dll 117 | 118 | 119 | ..\packages\Xamarin.Insights.1.11.3\lib\portable-win+net45+wp80+windows8+wpa+MonoAndroid10+MonoTouch10\Xamarin.Insights.dll 120 | 121 | 122 | ..\packages\Xam.Plugin.ExternalMaps.3.0.0\lib\portable-net45+wp8+wpa81+win8+MonoAndroid10+MonoTouch10+Xamarin.iOS10+UAP10\Plugin.ExternalMaps.dll 123 | 124 | 125 | ..\packages\Xam.Plugin.ExternalMaps.3.0.0\lib\portable-net45+wp8+wpa81+win8+MonoAndroid10+MonoTouch10+Xamarin.iOS10+UAP10\Plugin.ExternalMaps.Abstractions.dll 126 | 127 | 128 | ..\packages\FormsToolkit.1.1.11\lib\portable-net45+wp8+win8+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10+UAP10\FormsToolkit.dll 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | MSBuild:UpdateDesignTimeXaml 137 | 138 | 139 | MSBuild:UpdateDesignTimeXaml 140 | 141 | 142 | MSBuild:UpdateDesignTimeXaml 143 | 144 | 145 | MSBuild:UpdateDesignTimeXaml 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /Customers/Data/Customer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | using MvvmHelpers; 4 | 5 | namespace Customers 6 | { 7 | public class Customer : ObservableObject 8 | { 9 | public Customer() 10 | { 11 | Id = Guid.NewGuid().ToString(); 12 | PhotoUrl = "placeholderProfileImage"; 13 | } 14 | 15 | public string Id { get; set; } 16 | 17 | string _FirstName; 18 | public string FirstName 19 | { 20 | get { return _FirstName; } 21 | set 22 | { 23 | SetProperty(ref _FirstName, value); 24 | OnPropertyChanged(nameof(DisplayName)); // because DisplayName is dependent on FirstName, we need to manually call OnPropertyChanged() on DisplayName 25 | OnPropertyChanged(nameof(DisplayLastNameFirst)); // because DisplayLastNameFirst is dependent on FirstName, we need to manually call OnPropertyChanged() on DisplayLastNameFirst 26 | } 27 | } 28 | 29 | string _LastName; 30 | public string LastName 31 | { 32 | get { return _LastName; } 33 | set 34 | { 35 | SetProperty(ref _LastName, value); 36 | OnPropertyChanged(nameof(DisplayName)); // because DisplayName is dependent on LastName, we need to manually call OnPropertyChanged() on DisplayName 37 | OnPropertyChanged(nameof(DisplayLastNameFirst)); // because DisplayLastNameFirst is dependent on LastName, we need to manually call OnPropertyChanged() on DisplayLastNameFirst 38 | } 39 | } 40 | 41 | string _Company; 42 | public string Company 43 | { 44 | get { return _Company; } 45 | set { SetProperty(ref _Company, value); } 46 | } 47 | 48 | string _JobTitle; 49 | public string JobTitle 50 | { 51 | get { return _JobTitle; } 52 | set { SetProperty(ref _JobTitle, value); } 53 | } 54 | 55 | string _Email; 56 | public string Email 57 | { 58 | get { return _Email; } 59 | set { SetProperty(ref _Email, value); } 60 | } 61 | 62 | string _Phone; 63 | public string Phone 64 | { 65 | get { return _Phone; } 66 | set { SetProperty(ref _Phone, value); } 67 | } 68 | 69 | string _Street; 70 | public string Street 71 | { 72 | get { return _Street; } 73 | set 74 | { 75 | SetProperty(ref _Street, value); 76 | OnPropertyChanged(nameof(AddressString)); // because AddressString is dependent on Street, we need to manually call OnPropertyChanged() on AddressString 77 | } 78 | } 79 | 80 | string _City; 81 | public string City 82 | { 83 | get { return _City; } 84 | set 85 | { 86 | SetProperty(ref _City, value); 87 | OnPropertyChanged(nameof(AddressString)); // because AddressString is dependent on City, we need to manually call OnPropertyChanged() on AddressString 88 | } 89 | } 90 | 91 | string _PostalCode; 92 | public string PostalCode 93 | { 94 | get { return _PostalCode; } 95 | set 96 | { 97 | SetProperty(ref _PostalCode, value); 98 | OnPropertyChanged(nameof(AddressString)); // because AddressString is dependent on PostalCode, we need to manually call OnPropertyChanged() on AddressString 99 | OnPropertyChanged(nameof(StatePostal)); // because StatePostal is dependent on PostalCode, we need to manually call OnPropertyChanged() on StatePostal 100 | } 101 | } 102 | 103 | 104 | string _State; 105 | public string State 106 | { 107 | get { return _State; } 108 | set 109 | { 110 | SetProperty(ref _State, value); 111 | OnPropertyChanged(nameof(AddressString)); // because AddressString is dependent on State, we need to manually call OnPropertyChanged() on AddressString 112 | OnPropertyChanged(nameof(StatePostal)); // because StatePostal is dependent on State, we need to manually call OnPropertyChanged() on StatePostal 113 | } 114 | } 115 | 116 | string _PhotoUrl; 117 | public string PhotoUrl 118 | { 119 | get { return _PhotoUrl; } 120 | set 121 | { 122 | SetProperty(ref _PhotoUrl, value); 123 | OnPropertyChanged(nameof(SmallPhotoUrl)); // because SmallPhotoUrl is dependent on PhotoUrl, we need to manually call OnPropertyChanged() on SmallPhotoUrl 124 | } 125 | } 126 | 127 | public string SmallPhotoUrl { get { return PhotoUrl; }} 128 | 129 | [JsonIgnore] 130 | public string AddressString 131 | { 132 | get 133 | { 134 | return string.Format( 135 | "{0} {1} {2} {3}", 136 | Street, 137 | !string.IsNullOrWhiteSpace(City) ? City + "," : "", 138 | State, 139 | PostalCode); 140 | } 141 | } 142 | 143 | [JsonIgnore] 144 | public string DisplayName 145 | { 146 | get { return this.ToString(); } 147 | } 148 | 149 | [JsonIgnore] 150 | public string DisplayLastNameFirst 151 | { 152 | get { return String.Format("{0}, {1}", LastName, FirstName); } 153 | } 154 | 155 | [JsonIgnore] 156 | public string StatePostal 157 | { 158 | get { return State + " " + PostalCode; } 159 | } 160 | 161 | public override string ToString() 162 | { 163 | return FirstName + " " + LastName; 164 | } 165 | } 166 | } 167 | 168 | -------------------------------------------------------------------------------- /Customers/Data/IDataSource.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using System.Collections.Generic; 3 | 4 | namespace Customers 5 | { 6 | public interface IDataSource 7 | { 8 | Task SaveItem(T item); 9 | Task DeleteItem(string id); 10 | Task GetItem(string id); 11 | Task> GetItems(int start = 0, int count = 100, string query = ""); 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /Customers/Data/ILocalization.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Customers 4 | { 5 | public interface ILocalization 6 | { 7 | CultureInfo GetCurrentCultureInfo (); 8 | 9 | string ToTitleCase(string value); 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /Customers/Extensions/IntExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Customers 4 | { 5 | public static class IntExtensions 6 | { 7 | public static int RoundToLowestHundreds (this int i) 8 | { 9 | return ((int)Math.Floor(i / 100.0)) * 100; 10 | } 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /Customers/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Xamarin.Forms; 4 | using System.Linq; 5 | 6 | namespace Customers 7 | { 8 | public static class StringExtensions 9 | { 10 | public static string SanitizePhoneNumber(this string value) 11 | { 12 | return new String(value.ToCharArray().Where(Char.IsDigit).ToArray()); 13 | } 14 | 15 | public static string ToTitleCase(this string input) 16 | { 17 | return DependencyService.Get().ToTitleCase(input); 18 | } 19 | 20 | public static bool IsNullOrWhiteSpace(this string value) 21 | { 22 | return string.IsNullOrWhiteSpace(value); 23 | } 24 | } 25 | } 26 | 27 | 28 | -------------------------------------------------------------------------------- /Customers/Pages/CustomerDetailPage.xaml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 70 | 71 | 72 | 73 | 74 | 75 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /Customers/Pages/CustomerDetailPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using Xamarin.Forms; 2 | using Xamarin.Forms.Maps; 3 | using System.Threading.Tasks; 4 | using FormsToolkit; 5 | using Xamarin; 6 | using System; 7 | 8 | namespace Customers 9 | { 10 | public partial class CustomerDetailPage : ContentPage 11 | { 12 | protected CustomerDetailViewModel ViewModel 13 | { 14 | get { return BindingContext as CustomerDetailViewModel; } 15 | } 16 | 17 | public CustomerDetailPage() 18 | { 19 | InitializeComponent(); 20 | } 21 | 22 | async protected override void OnAppearing() 23 | { 24 | base.OnAppearing(); 25 | 26 | // Typically, is preferable to call into the viewmodel for OnAppearing() logic to be performed, 27 | // but we're not doing that in this case because we need to interact with the Xamarin.Forms.Map property on this Page. 28 | // In the future, the Map type and it's properties may get more binding support, so that the map setup can be omitted from code-behind. 29 | await SetupMap(); 30 | } 31 | 32 | /// 33 | /// Sets up the map. 34 | /// 35 | /// A Task. 36 | async Task SetupMap() 37 | { 38 | if (ViewModel.HasAddress) 39 | { 40 | 41 | ViewModel.IsBusy = true; 42 | Map.IsVisible = false; 43 | 44 | // set to a default position 45 | Position position; 46 | 47 | try 48 | { 49 | position = await ViewModel.GetPosition(); 50 | } 51 | catch (Exception ex) 52 | { 53 | MessagingService.Current.SendMessage(MessageKeys.DisplayGeocodingError); 54 | 55 | ViewModel.IsBusy = false; 56 | 57 | // TODO: Show insights 58 | Insights.Report(ex, Insights.Severity.Error); 59 | 60 | return; 61 | } 62 | 63 | // if lat and lon are both 0, then it's assumed that position acquisition failed 64 | if (position.Latitude == 0 && position.Longitude == 0) 65 | { 66 | MessagingService.Current.SendMessage(MessageKeys.DisplayGeocodingError); 67 | 68 | ViewModel.IsBusy = false; 69 | 70 | return; 71 | } 72 | else 73 | { 74 | var pin = new Pin() 75 | { 76 | Type = PinType.Place, 77 | Position = position, 78 | Label = ViewModel.Customer.DisplayName, 79 | Address = ViewModel.Customer.AddressString 80 | }; 81 | 82 | Map.Pins.Clear(); 83 | 84 | Map.Pins.Add(pin); 85 | 86 | Map.MoveToRegion(MapSpan.FromCenterAndRadius(pin.Position, Distance.FromMiles(10))); 87 | 88 | Map.IsVisible = true; 89 | ViewModel.IsBusy = false; 90 | } 91 | } 92 | } 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /Customers/Pages/CustomerEditPage.xaml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |