├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── run_tests.yml │ └── swiftlint.yml ├── .gitignore ├── LICENSE ├── NativeAppTemplate.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ └── NativeAppTemplate.xcscheme ├── NativeAppTemplate ├── .swiftlint.yml ├── App.swift ├── AppSingletons.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_free 1.png │ │ ├── icon_free 2.png │ │ └── icon_free.png │ ├── Colours │ │ ├── Backgrounds │ │ │ ├── Contents.json │ │ │ ├── backgroundColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── cardBackground.colorset │ │ │ │ └── Contents.json │ │ │ ├── coloredPrimaryBackground.colorset │ │ │ │ └── Contents.json │ │ │ ├── coloredPrimaryForeground.colorset │ │ │ │ └── Contents.json │ │ │ ├── coloredSecondaryBackground.colorset │ │ │ │ └── Contents.json │ │ │ ├── coloredSecondaryForeground.colorset │ │ │ │ └── Contents.json │ │ │ ├── failureBackground.colorset │ │ │ │ └── Contents.json │ │ │ ├── successBackground.colorset │ │ │ │ └── Contents.json │ │ │ └── successSecondaryForeground.colorset │ │ │ │ └── Contents.json │ │ ├── Button │ │ │ ├── Contents.json │ │ │ ├── coloredPrimaryButtonForeground.colorset │ │ │ │ └── Contents.json │ │ │ ├── coloredSecondaryButtonForeground.colorset │ │ │ │ └── Contents.json │ │ │ ├── destructiveButtonForeground.colorset │ │ │ │ └── Contents.json │ │ │ ├── failureSecondaryForeground.colorset │ │ │ │ └── Contents.json │ │ │ ├── primaryButtonForeground.colorset │ │ │ │ └── Contents.json │ │ │ └── secondaryButtonForeground.colorset │ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Snackbar │ │ │ ├── Contents.json │ │ │ ├── error.colorset │ │ │ │ └── Contents.json │ │ │ ├── snackText.colorset │ │ │ │ └── Contents.json │ │ │ ├── success.colorset │ │ │ │ └── Contents.json │ │ │ └── warning.colorset │ │ │ │ └── Contents.json │ │ ├── Tags │ │ │ ├── Contents.json │ │ │ ├── completedTagBackground.colorset │ │ │ │ └── Contents.json │ │ │ ├── completedTagBorder.colorset │ │ │ │ └── Contents.json │ │ │ ├── completedTagForeground.colorset │ │ │ │ └── Contents.json │ │ │ ├── customerScannedTagBackground.colorset │ │ │ │ └── Contents.json │ │ │ ├── customerScannedTagBorder.colorset │ │ │ │ └── Contents.json │ │ │ ├── customerScannedTagForeground.colorset │ │ │ │ └── Contents.json │ │ │ ├── idlingTagBackground.colorset │ │ │ │ └── Contents.json │ │ │ ├── idlingTagBorder.colorset │ │ │ │ └── Contents.json │ │ │ └── idlingTagForeground.colorset │ │ │ │ └── Contents.json │ │ ├── Text │ │ │ ├── Contents.json │ │ │ ├── coloredPrimaryFootnoteText.colorset │ │ │ │ └── Contents.json │ │ │ ├── coloredSecondaryFootnoteText.colorset │ │ │ │ └── Contents.json │ │ │ ├── contentText.colorset │ │ │ │ └── Contents.json │ │ │ ├── secondaryText.colorset │ │ │ │ └── Contents.json │ │ │ └── titleText.colorset │ │ │ │ └── Contents.json │ │ ├── Write to Tag │ │ │ ├── Contents.json │ │ │ ├── customerBackground.colorset │ │ │ │ └── Contents.json │ │ │ ├── customerForeground.colorset │ │ │ │ └── Contents.json │ │ │ ├── lockBackground.colorset │ │ │ │ └── Contents.json │ │ │ ├── lockForeground.colorset │ │ │ │ └── Contents.json │ │ │ ├── serverBackground.colorset │ │ │ │ └── Contents.json │ │ │ └── serverForeground.colorset │ │ │ │ └── Contents.json │ │ ├── accent.colorset │ │ │ └── Contents.json │ │ ├── alarm.colorset │ │ │ └── Contents.json │ │ └── lightestAccent.colorset │ │ │ └── Contents.json │ ├── Contents.json │ ├── Logo │ │ ├── Contents.json │ │ └── logo.imageset │ │ │ ├── Contents.json │ │ │ └── logo_white.svg │ └── Onboarding │ │ ├── Contents.json │ │ ├── onboarding1.imageset │ │ ├── Contents.json │ │ ├── overview1~universal@1x.png │ │ ├── overview1~universal@2x.png │ │ └── overview1~universal@3x.png │ │ ├── onboarding10.imageset │ │ ├── Contents.json │ │ ├── overview9~universal@1x.png │ │ ├── overview9~universal@2x.png │ │ └── overview9~universal@3x.png │ │ ├── onboarding11.imageset │ │ ├── Contents.json │ │ ├── overview9_phone_customer1~universal@1x.png │ │ ├── overview9_phone_customer1~universal@2x.png │ │ └── overview9_phone_customer1~universal@3x.png │ │ ├── onboarding12.imageset │ │ ├── Contents.json │ │ ├── overview13~universal@1x.png │ │ ├── overview13~universal@2x.png │ │ └── overview13~universal@3x.png │ │ ├── onboarding13.imageset │ │ ├── Contents.json │ │ ├── overview14~universal@1x.png │ │ ├── overview14~universal@2x.png │ │ └── overview14~universal@3x.png │ │ ├── onboarding1Slim.imageset │ │ ├── Contents.json │ │ ├── overview1_slim~universal@1x.png │ │ ├── overview1_slim~universal@2x.png │ │ └── overview1_slim~universal@3x.png │ │ ├── onboarding2.imageset │ │ ├── Contents.json │ │ ├── overview2~universal@1x.png │ │ ├── overview2~universal@2x.png │ │ └── overview2~universal@3x.png │ │ ├── onboarding3.imageset │ │ ├── Contents.json │ │ ├── overview6~universal@1x.png │ │ ├── overview6~universal@2x.png │ │ └── overview6~universal@3x.png │ │ ├── onboarding4.imageset │ │ ├── Contents.json │ │ ├── overview6_phone_customer2~universal@1x.png │ │ ├── overview6_phone_customer2~universal@2x.png │ │ └── overview6_phone_customer2~universal@3x.png │ │ ├── onboarding5.imageset │ │ ├── Contents.json │ │ ├── overview7~universal@1x.png │ │ ├── overview7~universal@2x.png │ │ └── overview7~universal@3x.png │ │ ├── onboarding6.imageset │ │ ├── Contents.json │ │ ├── overview8~universal@1x.png │ │ ├── overview8~universal@2x.png │ │ └── overview8~universal@3x.png │ │ ├── onboarding7.imageset │ │ ├── Contents.json │ │ ├── overview8_phone_server2~universal@1x.png │ │ ├── overview8_phone_server2~universal@2x.png │ │ └── overview8_phone_server2~universal@3x.png │ │ ├── onboarding8.imageset │ │ ├── Contents.json │ │ ├── overview8_phone_server3~universal@1x.png │ │ ├── overview8_phone_server3~universal@2x.png │ │ └── overview8_phone_server3~universal@3x.png │ │ └── onboarding9.imageset │ │ ├── Contents.json │ │ ├── overview8_2~universal@1x.png │ │ ├── overview8_2~universal@2x.png │ │ └── overview8_2~universal@3x.png ├── Constants.swift ├── Data │ ├── DataManager.swift │ ├── DataState.swift │ ├── Repositories │ │ ├── AccountPasswordRepository.swift │ │ ├── ItemTagRepository.swift │ │ └── ShopRepository.swift │ └── ViewModels │ │ └── TabViewModel.swift ├── Extensions │ ├── Bundle+Extensions.swift │ ├── Date+Extensions.swift │ ├── DateFormatter+Extensions.swift │ ├── String+Extensions.swift │ ├── UIApplication+DismissKeyboard.swift │ ├── UIImage+Extentions.swift │ └── View+Extensions.swift ├── Info.plist ├── Logging │ └── Logger.swift ├── Login │ ├── LoginRepository.swift │ ├── OnboardingRepository.swift │ ├── SessionRequest.swift │ ├── SessionsService.swift │ ├── SignUpRepository.swift │ ├── SignUpRequest.swift │ └── SignUpService.swift ├── Models │ ├── CompleteScanResult.swift │ ├── ItemTag.swift │ ├── ItemTagData.swift │ ├── ItemTagInfoFromNdefMessage.swift │ ├── ItemTagState.swift │ ├── ItemTagType.swift │ ├── MainTab.swift │ ├── Onboarding.swift │ ├── ScanResultError.swift │ ├── ScanState.swift │ ├── ScrollToTopID.swift │ ├── SendConfirmation.swift │ ├── SendResetPassword.swift │ ├── Shop.swift │ ├── Shopkeeper.swift │ ├── ShowTagInfoScanResult.swift │ ├── SignUp.swift │ └── UpdatePassword.swift ├── NFCManager.swift ├── NativeAppTemplate.entitlements ├── Networking │ ├── Adapters │ │ ├── DataCacheUpdate.swift │ │ ├── EntityAdapter.swift │ │ └── EntityAdapters │ │ │ ├── ItemTagAdapter.swift │ │ │ ├── ShopAdapter.swift │ │ │ ├── ShopkeeperAdapter.swift │ │ │ └── ShopkeeperSignInAdapter.swift │ ├── JSONAPI │ │ ├── JSONAPIDocument.swift │ │ ├── JSONAPIError.swift │ │ ├── JSONAPIErrorSource.swift │ │ ├── JSONAPIRelationship.swift │ │ └── JSONAPIResource.swift │ ├── Network │ │ ├── NativeAppTemplateAPI.swift │ │ └── NativeAppTemplateEnvironment.swift │ ├── Requests │ │ ├── AccountPasswordRequest.swift │ │ ├── ItemTagsRequest.swift │ │ ├── MeRequest.swift │ │ ├── Parameters.swift │ │ ├── PermissionsRequest.swift │ │ ├── Request.swift │ │ └── ShopsRequest.swift │ └── Services │ │ ├── AccountPasswordService.swift │ │ ├── ItemTagsService.swift │ │ ├── MeService.swift │ │ ├── PermissionsService.swift │ │ ├── Service.swift │ │ └── ShopsService.swift ├── Persistence │ └── KeychainStore │ │ ├── KeychainStore.swift │ │ ├── LoggedInShopkeeper.swift │ │ └── LoggedInShopkeeperKeychainStore.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── PrivacyInfo.xcprivacy ├── Sessions │ ├── SessionController+States.swift │ ├── SessionController.swift │ └── Shopkeeper+Backdoor.swift ├── Styleguide │ ├── Color+Extensions.swift │ ├── Font+Extensions.swift │ ├── Inter │ │ ├── Inter-Bold.ttf │ │ └── Inter-Medium.ttf │ ├── UIColor+Extensions.swift │ └── UIFont+Extensions.swift ├── TimeZoneData.swift ├── UI │ ├── App Root │ │ ├── AcceptPrivacyView.swift │ │ ├── AcceptTermsView.swift │ │ ├── AppTabView.swift │ │ ├── ForgotPasswordView.swift │ │ ├── MainView.swift │ │ ├── MessageBarView.swift │ │ ├── OnboardingView.swift │ │ ├── PermissionsLoadingView.swift │ │ ├── ResendConfirmationInstructionsView.swift │ │ ├── SignInEmailAndPasswordView.swift │ │ ├── SignUpOrSignInView.swift │ │ ├── SignUpView.swift │ │ └── SnackbarView.swift │ ├── Empty States │ │ ├── ErrorView.swift │ │ ├── LoadingView.swift │ │ ├── NeedAppUpdatesView.swift │ │ └── OfflineView.swift │ ├── Scan │ │ ├── CompleteScanResultView.swift │ │ ├── ScanView.swift │ │ └── ShowTagInfoScanResultView.swift │ ├── Settings │ │ ├── PasswordEditView.swift │ │ ├── SettingsView.swift │ │ └── ShopkeeperEditView.swift │ ├── Shared │ │ ├── MainButtonView.swift │ │ └── Tags │ │ │ ├── CompletedTag.swift │ │ │ ├── CustomerScannedTag.swift │ │ │ ├── IdlingTagView.swift │ │ │ └── TagView.swift │ ├── Shop Detail │ │ ├── ShopDetailCardView.swift │ │ └── ShopDetailView.swift │ ├── Shop List │ │ ├── ItemTag Detail │ │ │ ├── ItemTagDetailView.swift │ │ │ └── ItemTagEditView.swift │ │ ├── ItemTag List │ │ │ ├── ItemTagCreateView.swift │ │ │ ├── ItemTagListCardView.swift │ │ │ └── ItemTagListView.swift │ │ ├── ShopCreateView.swift │ │ ├── ShopListCardView.swift │ │ └── ShopListView.swift │ ├── Shop Settings │ │ ├── NumberTagsWebpageListView.swift │ │ ├── ShopBasicSettingsView.swift │ │ └── ShopSettingsView.swift │ └── UIKit │ │ └── MailView.swift └── Utilities │ ├── ImageSaver.swift │ ├── MessageBus.swift │ ├── QRCodeGenerator.swift │ └── Utility.swift ├── NativeAppTemplateTests ├── Models │ └── ShopkeeperTest.swift └── Networking │ └── Adapters │ ├── ItemTagAdapterTest.swift │ ├── ShopAdapterTest.swift │ ├── ShopkeeperAdapterTest.swift │ └── ShopkeeperSignInAdapterTest.swift ├── README.md ├── SECURITY.md ├── SampleCode.xcconfig └── docs └── images ├── nfc.gif ├── organization.gif ├── overview_after.png ├── overview_before.png ├── screenshots.png └── screenshots_nfc.png /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ### Steps to reproduce 7 | 8 | 9 | 10 | 11 | 12 | ### Expected behavior 13 | 14 | 15 | 16 | ### Actual behavior 17 | 18 | 19 | 20 | 21 | ### Environment 22 | 23 | 24 | 25 | * iOS Version: 26 | * NativeAppTemplate App Version: 27 | * Device: 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | -------------------------------------------------------------------------------- /.github/workflows/run_tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | build: 7 | runs-on: macos-15 8 | steps: 9 | - uses: maxim-lobanov/setup-xcode@v1 10 | with: 11 | xcode-version: '16.2.0' 12 | - uses: actions/checkout@v3 13 | - name: Unit Tests 14 | run: xcodebuild -project NativeAppTemplate.xcodeproj -scheme "NativeAppTemplate" -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' test 15 | -------------------------------------------------------------------------------- /.github/workflows/swiftlint.yml: -------------------------------------------------------------------------------- 1 | name: SwiftLint 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '.github/workflows/swiftlint.yml' 7 | - 'NativeAppTemplate/.swiftlint.yml' 8 | - 'NativeAppTemplate/**/*.swift' 9 | 10 | jobs: 11 | SwiftLint: 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 10 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: GitHub Action for SwiftLint (Only files changed in the PR) 18 | uses: norio-nomura/action-swiftlint@3.2.1 19 | with: 20 | args: --strict 21 | env: 22 | SWIFTLINT: true 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | **/fastlane/report.xml 66 | **/fastlane/Preview.html 67 | **/fastlane/screenshots/**/*.png 68 | **/fastlane/test_output 69 | **/GithubIgnore 70 | 71 | # Secrets 72 | secrets.*.xcconfig 73 | !secrets.template.xcconfig 74 | 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Daisuke Adachi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NativeAppTemplate.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NativeAppTemplate.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NativeAppTemplate.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "6b1231380720b7bc51910b56c46999018c0dc9e0af9279ee365eff309cd750a5", 3 | "pins" : [ 4 | { 5 | "identity" : "keychainaccess", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", 8 | "state" : { 9 | "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", 10 | "version" : "4.2.2" 11 | } 12 | }, 13 | { 14 | "identity" : "swift-collections", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/apple/swift-collections.git", 17 | "state" : { 18 | "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", 19 | "version" : "1.1.4" 20 | } 21 | }, 22 | { 23 | "identity" : "swiftyjson", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/SwiftyJSON/SwiftyJSON", 26 | "state" : { 27 | "revision" : "af76cf3ef710b6ca5f8c05f3a31307d44a3c5828", 28 | "version" : "5.0.2" 29 | } 30 | } 31 | ], 32 | "version" : 3 33 | } 34 | -------------------------------------------------------------------------------- /NativeAppTemplate/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | opt_in_rules: 2 | - array_init 3 | - closure_body_length 4 | - closure_spacing 5 | - closure_end_indentation 6 | - collection_alignment 7 | - conditional_returns_on_newline 8 | - contains_over_first_not_nil 9 | - convenience_type 10 | - empty_count 11 | - empty_xctest_method 12 | - explicit_init 13 | - extension_access_modifier 14 | - empty_string 15 | - fallthrough 16 | - fatal_error_message 17 | - first_where 18 | - implicit_return 19 | - joined_default_parameter 20 | - last_where 21 | - legacy_multiple 22 | - legacy_random 23 | - let_var_whitespace 24 | - literal_expression_end_indentation 25 | - lower_acl_than_parent 26 | - modifier_order 27 | - multiline_arguments 28 | - multiline_function_chains 29 | - multiline_parameters 30 | - nimble_operator 31 | - operator_usage_whitespace 32 | - overridden_super_call 33 | - private_action 34 | - prohibited_super_call 35 | - quick_discouraged_call 36 | - quick_discouraged_focused_test 37 | - quick_discouraged_pending_test 38 | - reduce_into 39 | - redundant_nil_coalescing 40 | - redundant_type_annotation 41 | - required_enum_case 42 | - single_test_class 43 | - sorted_first_last 44 | - strong_iboutlet 45 | - switch_case_on_newline 46 | - toggle_bool 47 | - unneeded_parentheses_in_closure_argument 48 | - untyped_error_in_catch 49 | - vertical_parameter_alignment_on_call 50 | - vertical_whitespace_closing_braces 51 | - xct_specific_matcher 52 | - yoda_condition 53 | 54 | disabled_rules: # rule identifiers to exclude from running 55 | - closure_parameter_position 56 | - force_cast 57 | - line_length 58 | - multiple_closures_with_trailing_closure 59 | - todo 60 | - trailing_whitespace 61 | - xctfail_message 62 | 63 | analyzer_rules: # only run with the analyze command 64 | - explicit_self 65 | - unused_import 66 | 67 | excluded: # paths to ignore during linting. overridden by `included` 68 | - Carthage 69 | - Pods 70 | 71 | closure_body_length: 72 | - 100 # warning 73 | - 200 # error 74 | 75 | cyclomatic_complexity: 76 | - 20 # warning 77 | - 25 # error 78 | 79 | large_tuple: 80 | - 3 # warning 81 | - 4 # error 82 | 83 | file_length: 84 | - 1200 # warning 85 | - 1500 # error 86 | 87 | function_body_length: 88 | - 100 # warning 89 | - 300 # error 90 | 91 | type_body_length: 92 | - 1000 # warning 93 | - 1500 # error 94 | 95 | type_name: 96 | allowed_symbols: 97 | - _ 98 | 99 | identifier_name: 100 | min_length: 3 101 | max_length: 75 102 | excluded: 103 | - by 104 | - id 105 | - db 106 | 107 | conditional_returns_on_newline: 108 | if_only: true 109 | -------------------------------------------------------------------------------- /NativeAppTemplate/App.swift: -------------------------------------------------------------------------------- 1 | // 2 | // App.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2024/10/01. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import TipKit 11 | 12 | @main 13 | struct App { 14 | typealias Objects = ( // swiftlint:disable:this large_tuple 15 | loginRepository: LoginRepository, 16 | sessionController: SessionController, 17 | dataManager: DataManager, 18 | messageBus: MessageBus 19 | ) 20 | 21 | private var loginRepository: LoginRepository 22 | private var sessionController: SessionController 23 | private var dataManager: DataManager 24 | private var messageBus: MessageBus 25 | 26 | @MainActor init() { 27 | // setup objects 28 | let nativeAppTemplateObjects = App.objects 29 | loginRepository = nativeAppTemplateObjects.loginRepository 30 | sessionController = nativeAppTemplateObjects.sessionController 31 | dataManager = nativeAppTemplateObjects.dataManager 32 | messageBus = nativeAppTemplateObjects.messageBus 33 | 34 | // Tips.showAllTipsForTesting() 35 | 36 | try? Tips.configure() 37 | } 38 | } 39 | 40 | // MARK: - SwiftUI.App 41 | extension App: SwiftUI.App { 42 | var body: some Scene { 43 | WindowGroup { 44 | ZStack { 45 | Rectangle() 46 | .fill(Color.backgroundColor) 47 | .edgesIgnoringSafeArea(.all) 48 | MainView() 49 | .preferredColorScheme(.dark) // Dark mode only 50 | .environment(loginRepository) 51 | .environment(sessionController) 52 | .environment(dataManager) 53 | .environment(messageBus) 54 | } 55 | } 56 | } 57 | } 58 | 59 | // MARK: - internal 60 | extension App { 61 | // Initialise the database 62 | @MainActor static var objects: Objects { 63 | let loginRepository = LoginRepository() 64 | let sessionController = SessionController(loginRepository: loginRepository) 65 | let messageBus = MessageBus() 66 | 67 | return ( 68 | loginRepository: loginRepository, 69 | sessionController: sessionController, 70 | dataManager: .init( 71 | sessionController: sessionController 72 | ), 73 | messageBus: messageBus 74 | ) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /NativeAppTemplate/AppSingletons.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @MainActor 4 | struct AppSingletons { 5 | var nfcManager: NFCManager 6 | 7 | init(nfcManager: NFCManager? = nil) { 8 | self.nfcManager = nfcManager ?? NFCManager.shared 9 | } 10 | } 11 | 12 | @MainActor var appSingletons = AppSingletons() 13 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_free.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "appearances" : [ 11 | { 12 | "appearance" : "luminosity", 13 | "value" : "dark" 14 | } 15 | ], 16 | "filename" : "icon_free 1.png", 17 | "idiom" : "universal", 18 | "platform" : "ios", 19 | "size" : "1024x1024" 20 | }, 21 | { 22 | "appearances" : [ 23 | { 24 | "appearance" : "luminosity", 25 | "value" : "tinted" 26 | } 27 | ], 28 | "filename" : "icon_free 2.png", 29 | "idiom" : "universal", 30 | "platform" : "ios", 31 | "size" : "1024x1024" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/AppIcon.appiconset/icon_free 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/AppIcon.appiconset/icon_free 1.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/AppIcon.appiconset/icon_free 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/AppIcon.appiconset/icon_free 2.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/AppIcon.appiconset/icon_free.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/AppIcon.appiconset/icon_free.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Backgrounds/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Backgrounds/backgroundColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x1E", 9 | "green" : "0x16", 10 | "red" : "0x14" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Backgrounds/cardBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x1E", 9 | "green" : "0x16", 10 | "red" : "0x14" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Backgrounds/coloredPrimaryBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0xF8", 10 | "red" : "0xE3" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Backgrounds/coloredPrimaryForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x88", 9 | "green" : "0x53", 10 | "red" : "0x03" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Backgrounds/coloredSecondaryBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFA", 9 | "green" : "0xF7", 10 | "red" : "0xF5" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Backgrounds/coloredSecondaryForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x33", 9 | "green" : "0x29", 10 | "red" : "0x1F" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Backgrounds/failureBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x42", 9 | "green" : "0x00", 10 | "red" : "0x62" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Backgrounds/successBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x40", 9 | "green" : "0x4D", 10 | "red" : "0x01" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Backgrounds/successSecondaryForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xE2", 9 | "green" : "0xF7", 10 | "red" : "0xC6" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Button/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Button/coloredPrimaryButtonForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x88", 9 | "green" : "0x53", 10 | "red" : "0x03" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Button/coloredSecondaryButtonForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x33", 9 | "green" : "0x29", 10 | "red" : "0x1F" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Button/destructiveButtonForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFA", 9 | "green" : "0xF7", 10 | "red" : "0xF5" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Button/failureSecondaryForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xD2", 9 | "green" : "0xB8", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Button/primaryButtonForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFA", 9 | "green" : "0xF7", 10 | "red" : "0xF5" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Button/secondaryButtonForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFA", 9 | "green" : "0xF7", 10 | "red" : "0xF5" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Snackbar/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Snackbar/error.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x1E", 9 | "green" : "0x16", 10 | "red" : "0x14" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Snackbar/snackText.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "srgb", 11 | "components" : { 12 | "red" : "1.000", 13 | "alpha" : "1.000", 14 | "blue" : "1.000", 15 | "green" : "1.000" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Snackbar/success.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x1E", 9 | "green" : "0x16", 10 | "red" : "0x14" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Snackbar/warning.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x00", 9 | "green" : "0x00", 10 | "red" : "0x00" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Tags/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Tags/completedTagBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xE2", 9 | "green" : "0xF7", 10 | "red" : "0xC6" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Tags/completedTagBorder.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Tags/completedTagForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x40", 9 | "green" : "0x4D", 10 | "red" : "0x01" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Tags/customerScannedTagBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xD2", 9 | "green" : "0xB8", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Tags/customerScannedTagBorder.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Tags/customerScannedTagForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x42", 9 | "green" : "0x00", 10 | "red" : "0x62" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Tags/idlingTagBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xEB", 9 | "green" : "0xE7", 10 | "red" : "0xE4" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Tags/idlingTagBorder.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Tags/idlingTagForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x33", 9 | "green" : "0x29", 10 | "red" : "0x1F" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Text/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Text/coloredPrimaryFootnoteText.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xA3", 9 | "green" : "0x69", 10 | "red" : "0x0B" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Text/coloredSecondaryFootnoteText.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x4B", 9 | "green" : "0x3F", 10 | "red" : "0x32" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Text/contentText.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.529", 9 | "green" : "0.463", 10 | "red" : "0.431" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "light" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.525", 27 | "green" : "0.463", 28 | "red" : "0.427" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | }, 33 | { 34 | "appearances" : [ 35 | { 36 | "appearance" : "luminosity", 37 | "value" : "dark" 38 | } 39 | ], 40 | "color" : { 41 | "color-space" : "srgb", 42 | "components" : { 43 | "alpha" : "1.000", 44 | "blue" : "0xB1", 45 | "green" : "0xA5", 46 | "red" : "0x9A" 47 | } 48 | }, 49 | "idiom" : "universal" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Text/secondaryText.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x7C", 9 | "green" : "0x6E", 10 | "red" : "0x61" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "light" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x7C", 27 | "green" : "0x6E", 28 | "red" : "0x61" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | }, 33 | { 34 | "appearances" : [ 35 | { 36 | "appearance" : "luminosity", 37 | "value" : "dark" 38 | } 39 | ], 40 | "color" : { 41 | "color-space" : "srgb", 42 | "components" : { 43 | "alpha" : "1.000", 44 | "blue" : "0x7C", 45 | "green" : "0x6E", 46 | "red" : "0x61" 47 | } 48 | }, 49 | "idiom" : "universal" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Text/titleText.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0xFF", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Write to Tag/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Write to Tag/customerBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0xF8", 10 | "red" : "0xE3" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Write to Tag/customerForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x88", 9 | "green" : "0x53", 10 | "red" : "0x03" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Write to Tag/lockBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xEA", 9 | "green" : "0xFB", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Write to Tag/lockForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x0B", 9 | "green" : "0x2B", 10 | "red" : "0x8D" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Write to Tag/serverBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xEC", 9 | "green" : "0xE3", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/Write to Tag/serverForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x42", 9 | "green" : "0x00", 10 | "red" : "0x62" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/accent.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFA", 9 | "green" : "0xD0", 10 | "red" : "0x5E" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/alarm.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x4E", 9 | "green" : "0x4E", 10 | "red" : "0xEF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Colours/lightestAccent.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0xF8", 10 | "red" : "0xE3" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Logo/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Logo/logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "logo_white.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "overview1~universal@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "overview1~universal@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "overview1~universal@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding1.imageset/overview1~universal@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding1.imageset/overview1~universal@1x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding1.imageset/overview1~universal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding1.imageset/overview1~universal@2x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding1.imageset/overview1~universal@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding1.imageset/overview1~universal@3x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding10.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "overview9~universal@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "overview9~universal@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "overview9~universal@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding10.imageset/overview9~universal@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding10.imageset/overview9~universal@1x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding10.imageset/overview9~universal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding10.imageset/overview9~universal@2x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding10.imageset/overview9~universal@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding10.imageset/overview9~universal@3x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding11.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "overview9_phone_customer1~universal@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "overview9_phone_customer1~universal@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "overview9_phone_customer1~universal@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding11.imageset/overview9_phone_customer1~universal@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding11.imageset/overview9_phone_customer1~universal@1x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding11.imageset/overview9_phone_customer1~universal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding11.imageset/overview9_phone_customer1~universal@2x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding11.imageset/overview9_phone_customer1~universal@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding11.imageset/overview9_phone_customer1~universal@3x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding12.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "overview13~universal@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "overview13~universal@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "overview13~universal@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding12.imageset/overview13~universal@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding12.imageset/overview13~universal@1x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding12.imageset/overview13~universal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding12.imageset/overview13~universal@2x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding12.imageset/overview13~universal@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding12.imageset/overview13~universal@3x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding13.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "overview14~universal@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "overview14~universal@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "overview14~universal@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding13.imageset/overview14~universal@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding13.imageset/overview14~universal@1x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding13.imageset/overview14~universal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding13.imageset/overview14~universal@2x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding13.imageset/overview14~universal@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding13.imageset/overview14~universal@3x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding1Slim.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "overview1_slim~universal@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "overview1_slim~universal@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "overview1_slim~universal@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding1Slim.imageset/overview1_slim~universal@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding1Slim.imageset/overview1_slim~universal@1x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding1Slim.imageset/overview1_slim~universal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding1Slim.imageset/overview1_slim~universal@2x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding1Slim.imageset/overview1_slim~universal@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding1Slim.imageset/overview1_slim~universal@3x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "overview2~universal@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "overview2~universal@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "overview2~universal@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding2.imageset/overview2~universal@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding2.imageset/overview2~universal@1x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding2.imageset/overview2~universal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding2.imageset/overview2~universal@2x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding2.imageset/overview2~universal@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding2.imageset/overview2~universal@3x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "overview6~universal@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "overview6~universal@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "overview6~universal@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding3.imageset/overview6~universal@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding3.imageset/overview6~universal@1x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding3.imageset/overview6~universal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding3.imageset/overview6~universal@2x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding3.imageset/overview6~universal@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding3.imageset/overview6~universal@3x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "overview6_phone_customer2~universal@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "overview6_phone_customer2~universal@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "overview6_phone_customer2~universal@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding4.imageset/overview6_phone_customer2~universal@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding4.imageset/overview6_phone_customer2~universal@1x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding4.imageset/overview6_phone_customer2~universal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding4.imageset/overview6_phone_customer2~universal@2x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding4.imageset/overview6_phone_customer2~universal@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding4.imageset/overview6_phone_customer2~universal@3x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding5.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "overview7~universal@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "overview7~universal@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "overview7~universal@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding5.imageset/overview7~universal@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding5.imageset/overview7~universal@1x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding5.imageset/overview7~universal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding5.imageset/overview7~universal@2x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding5.imageset/overview7~universal@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding5.imageset/overview7~universal@3x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding6.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "overview8~universal@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "overview8~universal@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "overview8~universal@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding6.imageset/overview8~universal@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding6.imageset/overview8~universal@1x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding6.imageset/overview8~universal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding6.imageset/overview8~universal@2x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding6.imageset/overview8~universal@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding6.imageset/overview8~universal@3x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding7.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "overview8_phone_server2~universal@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "overview8_phone_server2~universal@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "overview8_phone_server2~universal@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding7.imageset/overview8_phone_server2~universal@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding7.imageset/overview8_phone_server2~universal@1x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding7.imageset/overview8_phone_server2~universal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding7.imageset/overview8_phone_server2~universal@2x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding7.imageset/overview8_phone_server2~universal@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding7.imageset/overview8_phone_server2~universal@3x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding8.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "overview8_phone_server3~universal@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "overview8_phone_server3~universal@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "overview8_phone_server3~universal@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding8.imageset/overview8_phone_server3~universal@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding8.imageset/overview8_phone_server3~universal@1x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding8.imageset/overview8_phone_server3~universal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding8.imageset/overview8_phone_server3~universal@2x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding8.imageset/overview8_phone_server3~universal@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding8.imageset/overview8_phone_server3~universal@3x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding9.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "overview8_2~universal@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "overview8_2~universal@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "overview8_2~universal@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding9.imageset/overview8_2~universal@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding9.imageset/overview8_2~universal@1x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding9.imageset/overview8_2~universal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding9.imageset/overview8_2~universal@2x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Assets.xcassets/Onboarding/onboarding9.imageset/overview8_2~universal@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Assets.xcassets/Onboarding/onboarding9.imageset/overview8_2~universal@3x.png -------------------------------------------------------------------------------- /NativeAppTemplate/Data/DataManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataManager.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/02/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @MainActor @Observable class DataManager { 11 | 12 | // MARK: - Properties 13 | // Initialiser Arguments 14 | var sessionController: SessionController 15 | 16 | // Repositories 17 | private(set) var accountPasswordRepository: AccountPasswordRepository! 18 | private(set) var shopRepository: ShopRepository! 19 | private(set) var itemTagRepository: ItemTagRepository! 20 | private(set) var isRebuildingRepositories = false 21 | 22 | // MARK: - Initializers 23 | init(sessionController: SessionController) { 24 | self.sessionController = sessionController 25 | rebuildRepositories() 26 | } 27 | 28 | func rebuildRepositories() { 29 | isRebuildingRepositories = true 30 | 31 | withObservationTracking { 32 | _ = sessionController.client 33 | } onChange: { 34 | Task { @MainActor in 35 | self.rebuildRepositories() 36 | } 37 | } 38 | 39 | let accountPasswordService = AccountPasswordService(networkClient: sessionController.client) 40 | let shopsService = ShopsService(networkClient: sessionController.client) 41 | let itemTagsService = ItemTagsService(networkClient: sessionController.client) 42 | 43 | accountPasswordRepository = AccountPasswordRepository(accountPasswordService: accountPasswordService) 44 | shopRepository = ShopRepository(shopsService: shopsService) 45 | itemTagRepository = ItemTagRepository(itemTagsService: itemTagsService) 46 | 47 | isRebuildingRepositories = false 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /NativeAppTemplate/Data/DataState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataState.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/02/25. 6 | // 7 | 8 | enum DataState { 9 | case initial 10 | case loading 11 | case hasData 12 | case failed 13 | } 14 | -------------------------------------------------------------------------------- /NativeAppTemplate/Data/Repositories/AccountPasswordRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountPasswordRepository.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/02/25. 6 | // 7 | 8 | @MainActor class AccountPasswordRepository { 9 | let accountPasswordService: AccountPasswordService 10 | 11 | init( 12 | accountPasswordService: AccountPasswordService 13 | ) { 14 | self.accountPasswordService = accountPasswordService 15 | } 16 | 17 | func update(updatePassword: UpdatePassword) async throws { 18 | do { 19 | try await accountPasswordService.updatePassword(updatePassword: updatePassword) 20 | } catch { 21 | Failure 22 | .destroy(from: Self.self, reason: error.localizedDescription) 23 | .log() 24 | throw error 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /NativeAppTemplate/Data/Repositories/ShopRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShopRepository.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2022/06/28. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @MainActor @Observable class ShopRepository { 11 | let shopsService: ShopsService 12 | 13 | var shops: [Shop] = [] 14 | private(set) var state: DataState = .initial 15 | private(set) var limitCount = 0 16 | private(set) var createdShopsCount = 0 17 | 18 | init( 19 | shopsService: ShopsService 20 | ) { 21 | self.shopsService = shopsService 22 | } 23 | 24 | var isEmpty: Bool { shops.isEmpty } 25 | 26 | func findBy(id: String) -> Shop { 27 | let shop = shops.first { $0.id == id } 28 | return shop! 29 | } 30 | 31 | func reload() { 32 | if Task.isCancelled { 33 | return 34 | } 35 | 36 | if state == .loading { 37 | return 38 | } 39 | 40 | state = .loading 41 | 42 | Task { @MainActor in 43 | do { 44 | (shops, limitCount, createdShopsCount) = try await shopsService.allShops() 45 | state = .hasData 46 | } catch { 47 | state = .failed 48 | Failure 49 | .fetch(from: Self.self, reason: error.localizedDescription) 50 | .log() 51 | } 52 | } 53 | } 54 | 55 | func fetchDetail(id: String) async throws -> Shop { 56 | do { 57 | let shop = try await shopsService.shopDetail(id: id) 58 | let shopIndex = (shops.firstIndex { $0.id == shop.id }) 59 | if shopIndex != nil { 60 | shops[shopIndex!] = shop 61 | } 62 | 63 | return shop 64 | } catch { 65 | Failure 66 | .fetch(from: Self.self, reason: error.localizedDescription) 67 | .log() 68 | throw error 69 | } 70 | } 71 | 72 | func create(shop: Shop) async throws -> Shop { 73 | do { 74 | let createdShop = try await shopsService.makeShop(shop: shop) 75 | return createdShop 76 | } catch { 77 | Failure 78 | .create(from: Self.self, reason: error.localizedDescription) 79 | .log() 80 | throw error 81 | } 82 | } 83 | 84 | func update(id: String, shop: Shop) async throws -> Shop { 85 | do { 86 | let updatedShop = try await shopsService.updateShop(id: id, shop: shop) 87 | let shopIndex = (shops.firstIndex { $0.id == updatedShop.id }) 88 | if shopIndex != nil { 89 | shops[shopIndex!] = updatedShop 90 | } 91 | 92 | return updatedShop 93 | } catch { 94 | Failure 95 | .update(from: Self.self, reason: error.localizedDescription) 96 | .log() 97 | throw error 98 | } 99 | } 100 | 101 | func destroy(id: String) async throws { 102 | do { 103 | try await shopsService.destroyShop(id: id) 104 | } catch { 105 | Failure 106 | .destroy(from: Self.self, reason: error.localizedDescription) 107 | .log() 108 | throw error 109 | } 110 | } 111 | 112 | func reset(id: String) async throws { 113 | do { 114 | try await shopsService.resetShop(id: id) 115 | } catch { 116 | Failure 117 | .destroy(from: Self.self, reason: error.localizedDescription) 118 | .log() 119 | throw error 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /NativeAppTemplate/Data/ViewModels/TabViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabViewModel.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/02/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @Observable class TabViewModel { 11 | var selectedTab: MainTab = .shops 12 | 13 | var showingDetailView = Dictionary( 14 | uniqueKeysWithValues: zip(MainTab.allCases, AnyIterator { false }) 15 | ) 16 | } 17 | 18 | extension MainTab: EnvironmentKey { 19 | static var defaultValue: Self { .shops } 20 | } 21 | 22 | extension EnvironmentValues { 23 | var mainTab: MainTab { 24 | get { self[MainTab.self] } 25 | set { self[MainTab.self] = newValue } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /NativeAppTemplate/Extensions/Bundle+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+Extensions.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/11/13. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Bundle { 11 | public var appName: String { getInfo("CFBundleName") } 12 | public var displayName: String { getInfo("CFBundleDisplayName") } 13 | public var language: String { getInfo("CFBundleDevelopmentRegion") } 14 | public var identifier: String { getInfo("CFBundleIdentifier") } 15 | public var copyright: String { getInfo("NSHumanReadableCopyright").replacingOccurrences(of: "\\\\n", with: "\n") } 16 | 17 | public var appBuild: String { getInfo("CFBundleVersion") } 18 | public var appVersionLong: String { getInfo("CFBundleShortVersionString") } 19 | 20 | private func getInfo(_ str: String) -> String { infoDictionary?[str] as? String ?? "⚠️" } 21 | } 22 | -------------------------------------------------------------------------------- /NativeAppTemplate/Extensions/Date+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+Extensions.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/11/13. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Date { 11 | func dateByAddingNumberOfSeconds(_ seconds: Int) -> Date { 12 | let timeInterval = TimeInterval(seconds) 13 | return addingTimeInterval(timeInterval) 14 | } 15 | 16 | var cardDateString: String { 17 | let formatter = DateFormatter.cardDateFormatter 18 | return formatter.string(from: self) 19 | } 20 | 21 | var cardTimeString: String { 22 | let formatter = DateFormatter.cardTimeFormatter 23 | return formatter.string(from: self) 24 | } 25 | 26 | var cardTimeAgoInWordsDateString: String { 27 | let formatter = DateFormatter.timeAgoInWordsDateFormatter 28 | return formatter.string(from: self) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /NativeAppTemplate/Extensions/DateFormatter+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateFormatter+Extensions.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/11/13. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | static let cardDateString: String = "MMM dd yyyy" 12 | static let cardTimeString: String = "HH:mm" 13 | } 14 | 15 | extension ISO8601DateFormatter { 16 | convenience init(_ formatOptions: Options, timeZone: TimeZone = TimeZone(secondsFromGMT: 0)!) { 17 | self.init() 18 | self.formatOptions = formatOptions 19 | self.timeZone = timeZone 20 | } 21 | } 22 | extension Formatter { 23 | nonisolated(unsafe) static let iso8601 = ISO8601DateFormatter([.withInternetDateTime, .withFractionalSeconds]) 24 | static let isoDateUtc = { 25 | let dateFormatter = DateFormatter.formatter(for: "yyyy-MM-dd") 26 | dateFormatter.timeZone = NSTimeZone(name: "UTC")! as TimeZone 27 | return dateFormatter 28 | }() 29 | } 30 | 31 | extension String { 32 | var iso8601: Date? { 33 | Formatter.iso8601.date(from: self) 34 | } 35 | } 36 | 37 | extension DateFormatter { 38 | static let cardDateFormatter: DateFormatter = { 39 | DateFormatter.formatter(for: .cardDateString) 40 | }() 41 | 42 | static let cardTimeFormatter: DateFormatter = { 43 | DateFormatter.formatter(for: .cardTimeString) 44 | }() 45 | 46 | static let timeAgoInWordsDateFormatter: DateFormatter = { 47 | let dateFormatter = DateFormatter() 48 | dateFormatter.dateStyle = .short 49 | dateFormatter.timeStyle = .medium 50 | dateFormatter.doesRelativeDateFormatting = true 51 | return dateFormatter 52 | }() 53 | 54 | static func formatter(for dateString: String) -> DateFormatter { 55 | let dateFormatter = DateFormatter() 56 | dateFormatter.dateFormat = dateString 57 | return dateFormatter 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /NativeAppTemplate/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2024/01/04. 6 | // 7 | 8 | import UIKit 9 | 10 | extension String { 11 | /// Generates a `UIImage` instance from this string using a specified 12 | /// attributes and size. 13 | /// 14 | /// - Parameters: 15 | /// - attributes: to draw this string with. Default is `nil`. 16 | /// - size: of the image to return. 17 | /// - Returns: a `UIImage` instance from this string using a specified 18 | /// attributes and size, or `nil` if the operation fails. 19 | func image(withAttributes attributes: [NSAttributedString.Key: Any]? = nil, size: CGSize? = nil) -> UIImage? { 20 | let size = size ?? (self as NSString).size(withAttributes: attributes) 21 | return UIGraphicsImageRenderer(size: size).image { _ in 22 | (self as NSString).draw(in: CGRect(origin: .zero, size: size), 23 | withAttributes: attributes) 24 | } 25 | } 26 | 27 | func isAlphanumeric() -> Bool { 28 | self.rangeOfCharacter(from: CharacterSet.alphanumerics.inverted) == nil && !self.isEmpty 29 | } 30 | 31 | func isAlphanumeric(ignoreDiacritics: Bool = false) -> Bool { 32 | if ignoreDiacritics { 33 | return self.range(of: "[^a-zA-Z0-9]", options: .regularExpression) == nil && !self.isEmpty 34 | } else { 35 | return self.isAlphanumeric() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NativeAppTemplate/Extensions/UIApplication+DismissKeyboard.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Razeware LLC 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // Notwithstanding the foregoing, you may not use, copy, modify, merge, publish, 14 | // distribute, sublicense, create a derivative work, and/or sell copies of the 15 | // Software in any work that is designed, intended, or marketed for pedagogical or 16 | // instructional purposes related to programming, coding, application development, 17 | // or information technology. Permission for such use, copying, modification, 18 | // merger, publication, distribution, sublicensing, creation of derivative works, 19 | // or sale is expressly withheld. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | // THE SOFTWARE. 28 | 29 | import UIKit 30 | 31 | extension UIApplication { 32 | static func dismissKeyboard() { 33 | shared.dismissKeyboard() 34 | } 35 | 36 | private func dismissKeyboard() { 37 | sendAction( 38 | #selector(UIResponder.resignFirstResponder), 39 | to: nil, 40 | from: nil, 41 | for: nil) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /NativeAppTemplate/Extensions/UIImage+Extentions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Extentions.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/04. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIImage { 11 | func composited(withSmallCenterImage centerImage: UIImage) -> UIImage { 12 | UIGraphicsImageRenderer(size: self.size).image { context in 13 | let imageWidth = context.format.bounds.width 14 | let imageHeight = context.format.bounds.height 15 | let centerImageLength = imageWidth < imageHeight ? imageWidth / 5 : imageHeight / 5 16 | let centerImageRadius = centerImageLength * 0.2 17 | 18 | draw(in: CGRect(origin: CGPoint(x: 0, y: 0), 19 | size: context.format.bounds.size)) 20 | 21 | let centerImageRect = CGRect(x: (imageWidth - centerImageLength) / 2, 22 | y: (imageHeight - centerImageLength) / 2, 23 | width: centerImageLength, 24 | height: centerImageLength) 25 | 26 | let roundedRectPath = UIBezierPath(roundedRect: centerImageRect, 27 | cornerRadius: centerImageRadius) 28 | roundedRectPath.addClip() 29 | 30 | centerImage.draw(in: centerImageRect) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /NativeAppTemplate/Extensions/View+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Extensions.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2024/01/04. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | var inAllColorSchemes: some View { 12 | ForEach( 13 | ColorScheme.allCases, 14 | id: \.self, 15 | content: preferredColorScheme 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /NativeAppTemplate/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.nfc.readersession.iso7816.select-identifiers 6 | 7 | D2760000850101 8 | 9 | com.apple.developer.nfc.readersession.felica.systemcodes 10 | 11 | 12FC 12 | 13 | NSPhotoLibraryAddUsageDescription 14 | Save a QR code image including web page link used by this app. 15 | NFCReaderUsageDescription 16 | This app uses a NFC to write the application info to the NFC number tag or to read the NFC number tag into the application. 17 | CFBundleDevelopmentRegion 18 | $(DEVELOPMENT_LANGUAGE) 19 | CFBundleDisplayName 20 | NativeAppTemplate Free 21 | CFBundleExecutable 22 | $(EXECUTABLE_NAME) 23 | CFBundleIdentifier 24 | $(PRODUCT_BUNDLE_IDENTIFIER) 25 | CFBundleInfoDictionaryVersion 26 | 6.0 27 | CFBundleName 28 | $(PRODUCT_NAME) 29 | CFBundlePackageType 30 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 31 | CFBundleShortVersionString 32 | $(MARKETING_VERSION) 33 | CFBundleVersion 34 | $(CURRENT_PROJECT_VERSION) 35 | ITSAppUsesNonExemptEncryption 36 | 37 | LSRequiresIPhoneOS 38 | 39 | UIAppFonts 40 | 41 | Inter-Medium.ttf 42 | Inter-Bold.ttf 43 | 44 | UIApplicationSceneManifest 45 | 46 | UIApplicationSupportsMultipleScenes 47 | 48 | 49 | UIApplicationSupportsIndirectInputEvents 50 | 51 | UILaunchScreen 52 | 53 | UIColorName 54 | backgroundColor 55 | 56 | UIRequiredDeviceCapabilities 57 | 58 | arm64 59 | 60 | UIRequiresFullScreen 61 | 62 | UISupportedInterfaceOrientations 63 | 64 | UIInterfaceOrientationPortrait 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /NativeAppTemplate/Logging/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2024/01/04. 6 | // 7 | 8 | struct Failure { 9 | static func signUp(from source: Source.Type, reason: String) -> Self { 10 | .init(source: source, action: "signUp", reason: reason) 11 | } 12 | 13 | static func login(from source: Source.Type, reason: String) -> Self { 14 | .init(source: source, action: "login", reason: reason) 15 | } 16 | 17 | static func logout(from source: Source.Type, reason: String) -> Self { 18 | .init(source: source, action: "logout", reason: reason) 19 | } 20 | 21 | static func fetch(from source: Source.Type, reason: String) -> Self { 22 | .init(source: source, action: "fetch", reason: reason) 23 | } 24 | 25 | static func create(from source: Source.Type, reason: String) -> Self { 26 | .init(source: source, action: "create", reason: reason) 27 | } 28 | 29 | static func update(from source: Source.Type, reason: String) -> Self { 30 | .init(source: source, action: "update", reason: reason) 31 | } 32 | 33 | static func destroy(from source: Source.Type, reason: String) -> Self { 34 | .init(source: source, action: "destroy", reason: reason) 35 | } 36 | 37 | private init( 38 | source: Source.Type, 39 | action: String, 40 | reason: String 41 | ) { 42 | self.init( 43 | source: "\(Source.self)", 44 | action: action, 45 | reason: reason 46 | ) 47 | } 48 | 49 | private init( 50 | source: String, 51 | action: String, 52 | reason: String 53 | ) { 54 | self.source = source 55 | self.action = "Failed_\(action)" 56 | self.reason = reason 57 | } 58 | 59 | private let source: String 60 | private let action: String 61 | private let reason: String 62 | 63 | func log() { 64 | print( 65 | [ "source": source, 66 | "action": action, 67 | "reason": reason 68 | ] 69 | ) 70 | } 71 | } 72 | 73 | struct Event { 74 | static func login(from source: Source.Type) -> Self { 75 | .init( 76 | source: "\(Source.self)", 77 | action: "Login" 78 | ) 79 | } 80 | 81 | static func refresh( 82 | from source: Source.Type, 83 | action: String 84 | ) -> Self { 85 | .init( 86 | source: "\(Source.self)", 87 | action: "Refresh" 88 | ) 89 | } 90 | 91 | private let source: String 92 | private let action: String 93 | 94 | func log() { 95 | print("EVENT:: \(["source": source, "action": action])") 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /NativeAppTemplate/Login/LoginRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginRepository.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2021/01/11. 6 | // 7 | 8 | import Foundation 9 | 10 | @MainActor @Observable public class LoginRepository { 11 | // MARK: - Properties 12 | private var _currentShopkeeper: Shopkeeper? 13 | 14 | public var currentShopkeeper: Shopkeeper? { 15 | if _currentShopkeeper == nil { 16 | let keychainStore = LoggedInShopkeeperKeychainStore() 17 | 18 | do { 19 | let loggedInShopkeeper = try keychainStore.retrieve() 20 | _currentShopkeeper = Shopkeeper(from: loggedInShopkeeper) 21 | } catch { 22 | print(error) 23 | } 24 | } 25 | return _currentShopkeeper 26 | } 27 | 28 | @MainActor func login(email: String, password: String) async throws -> Shopkeeper { 29 | do { 30 | let sessionsService = SessionsService(networkClient: NativeAppTemplateAPI(authToken: "", client: "", expiry: "", uid: "", accountId: "")) 31 | let shopkeeper = try await sessionsService.makeSession(email: email, password: password) 32 | try saveShopkeeper(shopkeeper: shopkeeper) 33 | _currentShopkeeper = shopkeeper 34 | } catch { 35 | Failure 36 | .fetch(from: Self.self, reason: error.localizedDescription) 37 | .log() 38 | throw error 39 | } 40 | return currentShopkeeper! 41 | } 42 | 43 | @MainActor func logout(networkClient: NativeAppTemplateAPI) async throws { 44 | do { 45 | let sessionsService = SessionsService(networkClient: networkClient) 46 | try await sessionsService.destroySession() 47 | removeShopkeeper() 48 | _currentShopkeeper = .none 49 | } catch { 50 | Failure 51 | .fetch(from: Self.self, reason: error.localizedDescription) 52 | .log() 53 | removeShopkeeper() 54 | _currentShopkeeper = .none 55 | throw error 56 | } 57 | } 58 | 59 | public func updateShopkeeper(shopkeeper: Shopkeeper?) throws { 60 | _currentShopkeeper = shopkeeper 61 | if let shopkeeper = shopkeeper { 62 | try saveShopkeeper(shopkeeper: shopkeeper) 63 | } else { 64 | removeShopkeeper() 65 | } 66 | } 67 | 68 | private func saveShopkeeper(shopkeeper: Shopkeeper) throws { 69 | let keychainStore = LoggedInShopkeeperKeychainStore() 70 | let loggedInShopkeeper = LoggedInShopkeeper(from: shopkeeper) 71 | do { 72 | try keychainStore.store(loggedInShopkeeper) 73 | } catch { 74 | throw error 75 | } 76 | } 77 | 78 | private func removeShopkeeper() { 79 | let keychainStore = LoggedInShopkeeperKeychainStore() 80 | 81 | do { 82 | try keychainStore.remove() 83 | } catch { 84 | print(error) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /NativeAppTemplate/Login/OnboardingRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingRepository.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/04. 6 | // 7 | 8 | import Foundation 9 | import OrderedCollections 10 | 11 | @MainActor @Observable class OnboardingRepository { 12 | var onboardings: [Onboarding] = [] 13 | let onboardingsDictionary: OrderedDictionary = [ 14 | 1: false, 15 | 2: false, 16 | 3: false, 17 | 4: true, 18 | 5: false, 19 | 6: false, 20 | 7: true, 21 | 8: true, 22 | 9: false, 23 | 10: false, 24 | 11: true, 25 | 12: false, 26 | 13: false 27 | ] 28 | 29 | func reload() { 30 | onboardings = onboardingsDictionary.map { key, value in 31 | Onboarding(id: key, isPortraitImage: value) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /NativeAppTemplate/Login/SessionRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionRequest.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2021/01/11. 6 | // 7 | 8 | import Foundation 9 | import SwiftyJSON 10 | 11 | struct MakeSessionRequest: Request { 12 | typealias Response = Shopkeeper 13 | 14 | // MARK: - Properties 15 | var method: HTTPMethod { .POST } 16 | var path: String { "/shopkeeper_auth/sign_in" } 17 | var additionalHeaders: [String: String] = [:] 18 | var body: Data? { 19 | let json: [String: Any] = ["email": email, "password": password] 20 | return try? JSONSerialization.data(withJSONObject: json, options: .prettyPrinted) 21 | } 22 | 23 | // MARK: - Parameters 24 | let email: String 25 | let password: String 26 | 27 | func handle(response: Data) throws -> Shopkeeper { 28 | let json = try JSON(data: response) 29 | let doc = JSONAPIDocument(json) 30 | let shopkeepers = try doc.data.map { try ShopkeeperSignInAdapter.process(resource: $0) } 31 | return shopkeepers.first! 32 | } 33 | } 34 | 35 | struct DestroySessionRequest: Request { 36 | typealias Response = Void 37 | 38 | // MARK: - Properties 39 | var method: HTTPMethod { .DELETE } 40 | var path: String { "/shopkeeper_auth/sign_out" } 41 | var additionalHeaders: [String: String] = [:] 42 | 43 | var body: Data? { nil } 44 | 45 | // MARK: - Internal 46 | func handle(response: Data) throws { } 47 | } 48 | -------------------------------------------------------------------------------- /NativeAppTemplate/Login/SignUpRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpRepository.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2022/07/07. 6 | // 7 | 8 | import Foundation 9 | 10 | @MainActor class SignUpRepository { 11 | func signUp(signUp: SignUp) async throws -> Shopkeeper { 12 | var shopkeeper: Shopkeeper 13 | 14 | do { 15 | let signUpsService = SignUpsService(networkClient: NativeAppTemplateAPI(authToken: "", client: "", expiry: "", uid: "", accountId: "")) 16 | shopkeeper = try await signUpsService.makeShopkeeper(signUp: signUp) 17 | } catch { 18 | Failure 19 | .fetch(from: Self.self, reason: error.localizedDescription) 20 | .log() 21 | throw error 22 | } 23 | return shopkeeper 24 | } 25 | 26 | func update(id: String, signUp: SignUp, networkClient: NativeAppTemplateAPI) async throws -> Shopkeeper { 27 | var shopkeeper: Shopkeeper 28 | 29 | do { 30 | let signUpsService = SignUpsService(networkClient: networkClient) 31 | shopkeeper = try await signUpsService.updateShopkeeper(id: id, signUp: signUp) 32 | } catch { 33 | Failure 34 | .update(from: Self.self, reason: error.localizedDescription) 35 | .log() 36 | throw error 37 | } 38 | return shopkeeper 39 | } 40 | 41 | func destroy(networkClient: NativeAppTemplateAPI) async throws { 42 | do { 43 | let signUpsService = SignUpsService(networkClient: networkClient) 44 | try await signUpsService.destroyShopkeeper() 45 | removeShopkeeper() 46 | } catch { 47 | Failure 48 | .fetch(from: Self.self, reason: error.localizedDescription) 49 | .log() 50 | removeShopkeeper() 51 | 52 | throw error 53 | } 54 | } 55 | 56 | func sendResetPasswordInstruction(sendResetPassword: SendResetPassword) async throws { 57 | do { 58 | let signUpsService = SignUpsService(networkClient: NativeAppTemplateAPI(authToken: "", client: "", expiry: "", uid: "", accountId: "")) 59 | try await signUpsService.sendResetPasswordInstruction(sendResetPassword: sendResetPassword) 60 | } catch { 61 | Failure 62 | .fetch(from: Self.self, reason: error.localizedDescription) 63 | .log() 64 | 65 | throw error 66 | } 67 | } 68 | 69 | func sendConfirmationInstruction(sendConfirmation: SendConfirmation) async throws { 70 | do { 71 | let signUpsService = SignUpsService(networkClient: NativeAppTemplateAPI(authToken: "", client: "", expiry: "", uid: "", accountId: "")) 72 | try await signUpsService.sendConfirmationInstruction(sendConfirmation: sendConfirmation) 73 | } catch { 74 | Failure 75 | .fetch(from: Self.self, reason: error.localizedDescription) 76 | .log() 77 | throw error 78 | } 79 | } 80 | 81 | private func removeShopkeeper() { 82 | let keychainStore = LoggedInShopkeeperKeychainStore() 83 | 84 | do { 85 | try keychainStore.remove() 86 | } catch { 87 | print(error) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /NativeAppTemplate/Models/CompleteScanResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompleteScanResult.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/04. 6 | // 7 | 8 | import Foundation 9 | 10 | enum CompleteScanResultType { 11 | case idled 12 | case completed 13 | case reset 14 | case failed 15 | 16 | var displayString: String { 17 | switch self { 18 | case .idled: 19 | return "Idling" 20 | case .completed: 21 | return "Completed!" 22 | case .reset: 23 | return "Reset!" 24 | case .failed: 25 | return "Failed" 26 | } 27 | } 28 | } 29 | 30 | struct CompleteScanResult { 31 | var itemTag: ItemTag? 32 | var type: CompleteScanResultType = .idled 33 | var message = "" 34 | var scannedAt = Date.now 35 | } 36 | -------------------------------------------------------------------------------- /NativeAppTemplate/Models/ItemTag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemTag.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/01. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ItemTag: Codable, Hashable, Identifiable, Sendable { 11 | var id: String = "" 12 | var shopId: String = "" 13 | var queueNumber: String = "" 14 | var state = ItemTagState.idled 15 | var scanState = ScanState.unscanned 16 | var createdAt = Date.now 17 | var customerReadAt: Date? 18 | var completedAt: Date? 19 | var shopName: String = "" 20 | var alreadyCompleted: Bool? 21 | } 22 | 23 | extension ItemTag { 24 | func scanUrl(itemTagType: ItemTagType) -> URL { 25 | Utility.scanUrl(itemTagId: id, itemTagType: itemTagType.toJson()) 26 | } 27 | 28 | func toJson() -> [String: Any] { 29 | ["item_tag": 30 | [ 31 | "queue_number": queueNumber 32 | ] 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /NativeAppTemplate/Models/ItemTagData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemTagData.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/04. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ItemTagData: Identifiable { 11 | var id: String { 12 | itemTagId 13 | } 14 | var itemTagId: String 15 | var itemTagType: ItemTagType 16 | var isReadOnly: Bool 17 | var scannedAt: Date 18 | } 19 | 20 | // MARK: - Equatable 21 | extension ItemTagData: Equatable { 22 | static func == (lhs: ItemTagData, rhs: ItemTagData) -> Bool { 23 | lhs.itemTagId == rhs.itemTagId && 24 | lhs.itemTagType == rhs.itemTagType && 25 | lhs.isReadOnly == rhs.isReadOnly && 26 | lhs.scannedAt == rhs.scannedAt 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /NativeAppTemplate/Models/ItemTagInfoFromNdefMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemTagInfoFromNdefMessage.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/04. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ItemTagInfoFromNdefMessage { 11 | var id: String 12 | var type: String 13 | var success: Bool 14 | var message: String 15 | 16 | init() { 17 | self.id = "" 18 | self.type = "" 19 | self.success = false 20 | self.message = .messageWrittenOnTagIsWrong 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /NativeAppTemplate/Models/ItemTagState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemTagState.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/01. 6 | // 7 | 8 | enum ItemTagState: String, CaseIterable, Identifiable, Codable { 9 | case idled, 10 | completed 11 | 12 | var id: Self { self } 13 | 14 | init(string: String) { 15 | switch string { 16 | case "idled": 17 | self = .idled 18 | case "completed": 19 | self = .completed 20 | default: 21 | self = .idled 22 | } 23 | } 24 | 25 | var displayString: String { 26 | switch self { 27 | case .idled: 28 | return "Idling" 29 | case .completed: 30 | return "Completed" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /NativeAppTemplate/Models/ItemTagType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemTagType.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/01. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ItemTagType: String, CaseIterable, Identifiable, Codable { 11 | case server 12 | case customer 13 | 14 | var id: Self { self } 15 | 16 | init(string: String) { 17 | switch string { 18 | case "server": 19 | self = .server 20 | case "customer": 21 | self = .customer 22 | default: 23 | self = .server 24 | } 25 | } 26 | 27 | func toJson() -> String { 28 | switch self { 29 | case .server: 30 | return "server" 31 | case .customer: 32 | return "customer" 33 | } 34 | } 35 | 36 | var displayString: String { 37 | switch self { 38 | case .server: 39 | return "Server" 40 | case .customer: 41 | return "Customer" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /NativeAppTemplate/Models/MainTab.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainTab.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/11/03. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | enum MainTab { 12 | case shops 13 | case scan 14 | case settings 15 | } 16 | 17 | // MARK: - CaseIterable 18 | extension MainTab: CaseIterable { } 19 | -------------------------------------------------------------------------------- /NativeAppTemplate/Models/Onboarding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Onboarding.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/04. 6 | // 7 | 8 | struct Onboarding: Hashable, Codable, Identifiable { 9 | var id: Int 10 | var isPortraitImage: Bool = false 11 | } 12 | -------------------------------------------------------------------------------- /NativeAppTemplate/Models/ScanResultError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScanResultError.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/04. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ScanResultError: Error { 11 | case failed(String) 12 | } 13 | 14 | extension ScanResultError: LocalizedError { 15 | var errorDescription: String? { 16 | switch self { 17 | case .failed(let message): 18 | return message 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NativeAppTemplate/Models/ScanState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScanState.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/01. 6 | // 7 | 8 | enum ScanState: String, Identifiable, CaseIterable, Codable { 9 | case unscanned, 10 | scanned 11 | 12 | var id: Self { self } 13 | 14 | init(string: String) { 15 | switch string { 16 | case "unscanned": 17 | self = .unscanned 18 | case "scanned": 19 | self = .scanned 20 | default: 21 | self = .unscanned 22 | } 23 | } 24 | 25 | func toJson() -> String { 26 | switch self { 27 | case .unscanned: 28 | return "unscanned" 29 | case .scanned: 30 | return "scanned" 31 | } 32 | } 33 | 34 | var displayString: String { 35 | switch self { 36 | case .unscanned: 37 | return "Unscanned" 38 | case .scanned: 39 | return "Scanned" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /NativeAppTemplate/Models/ScrollToTopID.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollToTopID.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/11/03. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ScrollToTopID { 11 | let mainTab: MainTab 12 | let detail: Bool 13 | } 14 | 15 | extension ScrollToTopID: Hashable { } 16 | -------------------------------------------------------------------------------- /NativeAppTemplate/Models/SendConfirmation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SendConfirmation.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/09/30. 6 | // 7 | 8 | import Foundation 9 | 10 | struct SendConfirmation: Codable { 11 | var email: String 12 | var redirectUrl: String = NativeAppTemplateEnvironment.prod.baseURL.appendingPathComponent("/shopkeeper_auth/confirmation_result").absoluteString 13 | } 14 | 15 | extension SendConfirmation { 16 | func toJson() -> [String: Any] { 17 | [ 18 | "email": email, 19 | "redirect_url": redirectUrl 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /NativeAppTemplate/Models/SendResetPassword.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SendResetPassword.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/03/03. 6 | // 7 | 8 | import Foundation 9 | 10 | struct SendResetPassword: Codable { 11 | var email: String 12 | var redirectUrl: String = NativeAppTemplateEnvironment.prod.baseURL.appendingPathComponent("/shopkeeper_auth/reset_password/edit").absoluteString 13 | } 14 | 15 | extension SendResetPassword { 16 | func toJson() -> [String: Any] { 17 | [ 18 | "email": email, 19 | "redirect_url": redirectUrl 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /NativeAppTemplate/Models/Shop.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Shop.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2021/01/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Shop: Codable, Identifiable, Sendable { 11 | var id: String 12 | var name: String 13 | var description: String 14 | var timeZone: String 15 | var itemTagsCount: Int = 0 16 | var scannedItemTagsCount: Int = 0 17 | var completedItemTagsCount: Int = 0 18 | var displayShopServerPath: String = "" 19 | } 20 | 21 | extension Shop { 22 | var displayShopServerUrl: URL { 23 | URL(string: "\(NativeAppTemplateEnvironment.prod.baseURL.absoluteString)\(displayShopServerPath)")! 24 | } 25 | 26 | func toJsonForCreate() -> [String: Any] { 27 | [ 28 | "shop": [ 29 | "name": name, 30 | "description": description, 31 | "time_zone": timeZone 32 | ] as [String: Any] 33 | ] 34 | } 35 | 36 | func toJsonForUpdate() -> [String: Any] { 37 | [ 38 | "shop": [ 39 | "name": name, 40 | "description": description, 41 | "time_zone": timeZone 42 | ] as [String: Any] 43 | ] 44 | } 45 | } 46 | 47 | extension Shop: Hashable { 48 | static func == (lhs: Shop, rhs: Shop) -> Bool { 49 | lhs.id == rhs.id && 50 | lhs.name == rhs.name && 51 | lhs.description == rhs.description && 52 | lhs.timeZone == rhs.timeZone && 53 | lhs.itemTagsCount == rhs.itemTagsCount && 54 | lhs.scannedItemTagsCount == rhs.scannedItemTagsCount && 55 | lhs.completedItemTagsCount == rhs.completedItemTagsCount && 56 | lhs.displayShopServerPath == rhs.displayShopServerPath 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /NativeAppTemplate/Models/ShowTagInfoScanResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShowTagInfoScanResult.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/04. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ShowTagInfoScanResultType { 11 | case idled 12 | case succeeded 13 | case failed 14 | } 15 | 16 | struct ShowTagInfoScanResult { 17 | var itemTag: ItemTag? 18 | var itemTagType: ItemTagType = .server 19 | var isReadOnly = false 20 | var type: ShowTagInfoScanResultType = .idled 21 | var message = "" 22 | var scannedAt = Date.now 23 | } 24 | -------------------------------------------------------------------------------- /NativeAppTemplate/Models/SignUp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUp.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | struct SignUp: Codable { 11 | var name: String 12 | var email: String 13 | var timeZone: String 14 | var currentPlatform: String = "ios" 15 | var password: String? 16 | } 17 | 18 | extension SignUp { 19 | func toJsonForCreate() -> [String: Any] { 20 | [ 21 | "name": name, 22 | "email": email, 23 | "time_zone": timeZone, 24 | "current_platform": currentPlatform, 25 | "password": password! 26 | ] 27 | } 28 | 29 | func toJsonForUpdate() -> [String: Any] { 30 | [ 31 | "name": name, 32 | "email": email, 33 | "time_zone": timeZone 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /NativeAppTemplate/Models/UpdatePassword.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdatePassword.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/02/25. 6 | // 7 | 8 | import Foundation 9 | 10 | struct UpdatePassword: Codable { 11 | var currentPassword: String 12 | var password: String 13 | var passwordConfirmation: String 14 | } 15 | 16 | extension UpdatePassword { 17 | func toJson() -> [String: Any] { 18 | [ "shopkeeper": 19 | [ 20 | "current_password": currentPassword, 21 | "password": password, 22 | "password_confirmation": passwordConfirmation 23 | ] 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /NativeAppTemplate/NativeAppTemplate.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.associated-domains 6 | 7 | applinks:api.nativeapptemplate.com 8 | 9 | com.apple.developer.nfc.readersession.formats 10 | 11 | TAG 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /NativeAppTemplate/Networking/Adapters/EntityAdapters/ItemTagAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemTagAdapter.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/01. 6 | // 7 | 8 | import struct Foundation.URL 9 | 10 | struct ItemTagAdapter: EntityAdapter { 11 | static func process(resource: JSONAPIResource, relationships: [EntityRelationship] = [], cacheUpdate: DataCacheUpdate = DataCacheUpdate()) throws -> ItemTag { 12 | guard resource.entityType == .itemTag else { throw EntityAdapterError.invalidResourceTypeForAdapter } 13 | 14 | guard let shopId = resource.attributes["shop_id"] as? String, 15 | let queueNumber = resource.attributes["queue_number"] as? String, 16 | let state = resource.attributes["state"] as? String, 17 | let scanState = resource.attributes["scan_state"] as? String, 18 | let createdAtString = resource.attributes["created_at"] as? String, 19 | let shopName = resource.attributes["shop_name"] as? String 20 | else { 21 | throw EntityAdapterError.invalidOrMissingAttributes 22 | } 23 | 24 | let createdAt = createdAtString.iso8601! 25 | 26 | let customerReadAtString = resource.attributes["customer_read_at"] as? String 27 | let customerReadAt = customerReadAtString?.iso8601 28 | 29 | let completedAtString = resource.attributes["completed_at"] as? String 30 | let completedAt = completedAtString?.iso8601 31 | 32 | let alreadyCompleted = resource.attributes["already_completed"] as? Bool 33 | 34 | return ItemTag( 35 | id: resource.id, 36 | shopId: shopId, 37 | queueNumber: queueNumber, 38 | state: ItemTagState(string: state), 39 | scanState: ScanState(string: scanState), 40 | createdAt: createdAt, 41 | customerReadAt: customerReadAt, 42 | completedAt: completedAt, 43 | shopName: shopName, 44 | alreadyCompleted: alreadyCompleted 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /NativeAppTemplate/Networking/Adapters/EntityAdapters/ShopAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShopAdapter.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2022/06/28. 6 | // 7 | 8 | import struct Foundation.URL 9 | 10 | struct ShopAdapter: EntityAdapter { 11 | static func process(resource: JSONAPIResource, relationships: [EntityRelationship] = [], cacheUpdate: DataCacheUpdate = DataCacheUpdate()) throws -> Shop { 12 | guard resource.entityType == .shop else { throw EntityAdapterError.invalidResourceTypeForAdapter } 13 | 14 | guard let name = resource.attributes["name"] as? String, 15 | let timeZone = resource.attributes["time_zone"] as? String, 16 | let displayShopServerPath = resource.attributes["display_shop_server_path"] as? String 17 | else { 18 | throw EntityAdapterError.invalidOrMissingAttributes 19 | } 20 | 21 | return Shop( 22 | id: resource.id, 23 | name: name, 24 | description: resource.attributes["description"] as? String ?? "", 25 | timeZone: timeZone, 26 | itemTagsCount: resource.attributes["item_tags_count"] as? Int ?? 0, 27 | scannedItemTagsCount: resource.attributes["scanned_item_tags_count"] as? Int ?? 0, 28 | completedItemTagsCount: resource.attributes["completed_item_tags_count"] as? Int ?? 0, 29 | displayShopServerPath: displayShopServerPath 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /NativeAppTemplate/Networking/Adapters/EntityAdapters/ShopkeeperAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShopkeeperAdapter.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2021/01/16. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ShopkeeperAdapter: EntityAdapter { 11 | static func process(resource: JSONAPIResource, relationships: [EntityRelationship] = [], cacheUpdate: DataCacheUpdate = DataCacheUpdate()) throws -> Shopkeeper { 12 | guard resource.entityType == .shopkeeper else { 13 | throw EntityAdapterError.invalidResourceTypeForAdapter 14 | } 15 | 16 | guard let email = resource.attributes["email"] as? String, 17 | let name = resource.attributes["name"] as? String, 18 | let timeZone = resource.attributes["time_zone"] as? String 19 | else { 20 | throw EntityAdapterError.invalidOrMissingAttributes 21 | } 22 | 23 | return Shopkeeper( 24 | id: resource.id, 25 | accountId: "", 26 | personalAccountId: "", 27 | accountOwnerId: "", 28 | accountName: "", 29 | email: email, 30 | name: name, 31 | timeZone: timeZone, 32 | uid: "", 33 | token: "", 34 | client: "", 35 | expiry: "" 36 | )! 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NativeAppTemplate/Networking/Adapters/EntityAdapters/ShopkeeperSignInAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShopkeeperSignInAdapter.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2022/08/11. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ShopkeeperSignInAdapter: EntityAdapter { 11 | static func process(resource: JSONAPIResource, relationships: [EntityRelationship] = [], cacheUpdate: DataCacheUpdate = DataCacheUpdate()) throws -> Shopkeeper { 12 | guard resource.entityType == .shopkeeperSignIn else { 13 | throw EntityAdapterError.invalidResourceTypeForAdapter 14 | } 15 | 16 | guard let accountId = resource.attributes["account_id"] as? String, 17 | let personalAccountId = resource.attributes["personal_account_id"] as? String, 18 | let accountOwnerId = resource.attributes["account_owner_id"] as? String, 19 | let accountName = resource.attributes["account_name"] as? String, 20 | let email = resource.attributes["email"] as? String, 21 | let name = resource.attributes["name"] as? String, 22 | let timeZone = resource.attributes["time_zone"] as? String, 23 | let uid = resource.attributes["uid"] as? String 24 | else { 25 | throw EntityAdapterError.invalidOrMissingAttributes 26 | } 27 | 28 | return Shopkeeper( 29 | id: resource.id, 30 | accountId: accountId, 31 | personalAccountId: personalAccountId, 32 | accountOwnerId: accountOwnerId, 33 | accountName: accountName, 34 | email: email, 35 | name: name, 36 | timeZone: timeZone, 37 | uid: uid, 38 | token: resource.attributes["token"] as? String ?? "", 39 | client: resource.attributes["client"] as? String ?? "", 40 | expiry: resource.attributes["expiry"] as? String ?? "" 41 | )! 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /NativeAppTemplate/Networking/JSONAPI/JSONAPIError.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Razeware LLC 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // Notwithstanding the foregoing, you may not use, copy, modify, merge, publish, 14 | // distribute, sublicense, create a derivative work, and/or sell copies of the 15 | // Software in any work that is designed, intended, or marketed for pedagogical or 16 | // instructional purposes related to programming, coding, application development, 17 | // or information technology. Permission for such use, copying, modification, 18 | // merger, publication, distribution, sublicensing, creation of derivative works, 19 | // or sale is expressly withheld. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | // THE SOFTWARE. 28 | 29 | import struct Foundation.URL 30 | import SwiftyJSON 31 | 32 | public class JSONAPIError { 33 | // MARK: - Properties 34 | var id: String = "" 35 | var links: [String: URL] = [:] 36 | var status: String = "" 37 | var code: String = "" 38 | var title: String = "" 39 | var detail: String = "" 40 | var source: JSONAPIErrorSource? 41 | var meta: [String: Any] = [:] 42 | 43 | // MARK: - Initializers 44 | convenience init(_ json: JSON) { 45 | self.init() 46 | 47 | id = json["id"].stringValue 48 | 49 | if let linksDict = json["links"].dictionaryObject { 50 | for link in linksDict { 51 | if let strValue = link.value as? String, 52 | let url = URL(string: strValue) { 53 | links[link.key] = url 54 | } 55 | } 56 | } 57 | 58 | status = json["status"].stringValue 59 | code = json["code"].stringValue 60 | title = json["title"].stringValue 61 | detail = json["detail"].stringValue 62 | source = JSONAPIErrorSource(json["source"]) 63 | meta = json["meta"].dictionaryValue 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /NativeAppTemplate/Networking/JSONAPI/JSONAPIErrorSource.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Razeware LLC 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // Notwithstanding the foregoing, you may not use, copy, modify, merge, publish, 14 | // distribute, sublicense, create a derivative work, and/or sell copies of the 15 | // Software in any work that is designed, intended, or marketed for pedagogical or 16 | // instructional purposes related to programming, coding, application development, 17 | // or information technology. Permission for such use, copying, modification, 18 | // merger, publication, distribution, sublicensing, creation of derivative works, 19 | // or sale is expressly withheld. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | // THE SOFTWARE. 28 | 29 | import SwiftyJSON 30 | 31 | public class JSONAPIErrorSource { 32 | // MARK: - Properties 33 | var pointer: String = "" 34 | var parameter: String = "" 35 | 36 | // MARK: - Initializers 37 | convenience init(_ json: JSON) { 38 | self.init() 39 | 40 | pointer = json["pointer"].stringValue 41 | parameter = json["parameter"].stringValue 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /NativeAppTemplate/Networking/JSONAPI/JSONAPIRelationship.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Razeware LLC 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // Notwithstanding the foregoing, you may not use, copy, modify, merge, publish, 14 | // distribute, sublicense, create a derivative work, and/or sell copies of the 15 | // Software in any work that is designed, intended, or marketed for pedagogical or 16 | // instructional purposes related to programming, coding, application development, 17 | // or information technology. Permission for such use, copying, modification, 18 | // merger, publication, distribution, sublicensing, creation of derivative works, 19 | // or sale is expressly withheld. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | // THE SOFTWARE. 28 | 29 | import struct Foundation.URL 30 | import SwiftyJSON 31 | 32 | public class JSONAPIRelationship { 33 | // MARK: - Properties 34 | var meta: [String: Any] = [:] 35 | var data: [JSONAPIResource] = [] 36 | var links: [String: URL] = [:] 37 | var type: String = "" 38 | 39 | // MARK: - Initializers 40 | convenience init(_ json: JSON, 41 | type: String, 42 | parent: JSONAPIDocument?) { 43 | self.init() 44 | 45 | self.type = type 46 | meta = json["meta"].dictionaryObject ?? [:] 47 | self.data = json["data"].arrayValue.map { 48 | JSONAPIResource($0, parent: nil) 49 | } 50 | 51 | let nonArrayJSON = json["data"] 52 | let nonArrayJSONAPIResource = JSONAPIResource(nonArrayJSON, parent: nil) 53 | data.append(nonArrayJSONAPIResource) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /NativeAppTemplate/Networking/Network/NativeAppTemplateEnvironment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NativeAppTemplate.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2022/07/03. 6 | // 7 | 8 | import struct Foundation.URL 9 | 10 | struct NativeAppTemplateEnvironment: Equatable { 11 | 12 | // MARK: - Properties 13 | var baseURL: URL 14 | let basePath = "/api/v1" 15 | } 16 | 17 | extension NativeAppTemplateEnvironment { 18 | static let urlString = if String.port.isEmpty { 19 | "\(String.scheme)://\(String.domain)" 20 | } else { 21 | "\(String.scheme)://\(String.domain):\(String.port)" 22 | } 23 | 24 | static let prod = NativeAppTemplateEnvironment(baseURL: URL(string: urlString)!) 25 | } 26 | -------------------------------------------------------------------------------- /NativeAppTemplate/Networking/Requests/AccountPasswordRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountPasswordRequest.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/02/25. 6 | // 7 | 8 | import Foundation 9 | import SwiftyJSON 10 | 11 | struct UpdateAccountPasswordRequest: Request { 12 | typealias Response = Void 13 | 14 | // MARK: - Properties 15 | var method: HTTPMethod { .PATCH } 16 | var path: String { "/shopkeeper/account/password" } 17 | var additionalHeaders: [String: String] = [:] 18 | var body: Data? { 19 | let json = updatePassword.toJson() 20 | return try? JSONSerialization.data(withJSONObject: json) 21 | } 22 | 23 | let updatePassword: UpdatePassword 24 | 25 | // MARK: - Internal 26 | func handle(response: Data) throws { } 27 | } 28 | -------------------------------------------------------------------------------- /NativeAppTemplate/Networking/Requests/MeRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeRequest.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/12/23. 6 | // 7 | 8 | import Foundation 9 | import SwiftyJSON 10 | 11 | struct UpdateConfirmedPrivacyVersionRequest: Request { 12 | typealias Response = Void 13 | 14 | // MARK: - Properties 15 | var method: HTTPMethod { .PATCH } 16 | var path: String { "/shopkeeper/me/update_confirmed_privacy_version" } 17 | var additionalHeaders: [String: String] = [:] 18 | var body: Data? { nil } 19 | 20 | // MARK: - Internal 21 | func handle(response: Data) throws { } 22 | } 23 | 24 | struct UpdateConfirmedTermsVersionRequest: Request { 25 | typealias Response = Void 26 | 27 | // MARK: - Properties 28 | var method: HTTPMethod { .PATCH } 29 | var path: String { "/shopkeeper/me/update_confirmed_terms_version" } 30 | var additionalHeaders: [String: String] = [:] 31 | var body: Data? { nil } 32 | 33 | // MARK: - Internal 34 | func handle(response: Data) throws { } 35 | } 36 | -------------------------------------------------------------------------------- /NativeAppTemplate/Networking/Requests/Parameters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Parameter.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2022/06/28. 6 | // 7 | 8 | struct Parameter: Hashable, Codable { 9 | let key: String 10 | let value: String 11 | } 12 | -------------------------------------------------------------------------------- /NativeAppTemplate/Networking/Requests/Request.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Razeware LLC 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // Notwithstanding the foregoing, you may not use, copy, modify, merge, publish, 14 | // distribute, sublicense, create a derivative work, and/or sell copies of the 15 | // Software in any work that is designed, intended, or marketed for pedagogical or 16 | // instructional purposes related to programming, coding, application development, 17 | // or information technology. Permission for such use, copying, modification, 18 | // merger, publication, distribution, sublicensing, creation of derivative works, 19 | // or sale is expressly withheld. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | // THE SOFTWARE. 28 | 29 | import struct Foundation.Data 30 | import SwiftyJSON 31 | 32 | enum HTTPMethod: String { 33 | case GET 34 | case POST 35 | case PUT 36 | case DELETE 37 | case PATCH 38 | } 39 | 40 | protocol Request { 41 | associatedtype Response 42 | 43 | var method: HTTPMethod { get } 44 | var path: String { get } 45 | var additionalHeaders: [String: String] { get } 46 | var body: Data? { get } 47 | 48 | func handle(response: Data) throws -> Response 49 | } 50 | 51 | // Default implementation to .GET 52 | extension Request { 53 | var method: HTTPMethod { .GET } 54 | var body: Data? { nil } 55 | } 56 | -------------------------------------------------------------------------------- /NativeAppTemplate/Networking/Services/AccountPasswordService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountPasswordService.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/02/25. 6 | // 7 | 8 | import class Foundation.URLSession 9 | 10 | struct AccountPasswordService: Service { 11 | let networkClient: NativeAppTemplateAPI 12 | let session = URLSession(configuration: .default) 13 | } 14 | 15 | extension AccountPasswordService { 16 | func updatePassword(updatePassword: UpdatePassword) async throws -> UpdateAccountPasswordRequest.Response { 17 | let request = UpdateAccountPasswordRequest(updatePassword: updatePassword) 18 | return try await makeRequest(request: request) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeAppTemplate/Networking/Services/ItemTagsService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemTagsService.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/01. 6 | // 7 | 8 | import class Foundation.URLSession 9 | 10 | struct ItemTagsService: Service { 11 | let networkClient: NativeAppTemplateAPI 12 | let session = URLSession(configuration: .default) 13 | } 14 | 15 | extension ItemTagsService { 16 | // MARK: - Internal 17 | func allItemTags(shopId: String) async throws -> GetItemTagsRequest.Response { 18 | let request = GetItemTagsRequest(shopId: shopId) 19 | return try await makeRequest(request: request) 20 | } 21 | 22 | func itemTagDetail(id: String) async throws -> GetItemTagDetailRequest.Response { 23 | try await makeRequest(request: GetItemTagDetailRequest(id: id)) 24 | } 25 | 26 | func makeItemTag(shopId: String, itemTag: ItemTag) async throws -> MakeItemTagRequest.Response { 27 | let request = MakeItemTagRequest(shopId: shopId, itemTag: itemTag) 28 | return try await makeRequest(request: request) 29 | } 30 | 31 | func updateItemTag(id: String, itemTag: ItemTag) async throws -> UpdateItemTagRequest.Response { 32 | let request = UpdateItemTagRequest(id: id, itemTag: itemTag) 33 | return try await makeRequest(request: request) 34 | } 35 | 36 | func destroyItemTag(id: String) async throws -> DestroyItemTagRequest.Response { 37 | try await makeRequest(request: DestroyItemTagRequest(id: id)) 38 | } 39 | 40 | func completeItemTag(id: String) async throws -> CompleteItemTagRequest.Response { 41 | try await makeRequest(request: CompleteItemTagRequest(id: id)) 42 | } 43 | 44 | func resetItemTag(id: String) async throws -> ResetItemTagRequest.Response { 45 | try await makeRequest(request: ResetItemTagRequest(id: id)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /NativeAppTemplate/Networking/Services/MeService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeService.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/12/23. 6 | // 7 | 8 | import class Foundation.URLSession 9 | 10 | struct MeService: Service { 11 | let networkClient: NativeAppTemplateAPI 12 | let session = URLSession(configuration: .default) 13 | } 14 | 15 | // MARK: - Internal 16 | extension MeService { 17 | func updateConfirmedPrivacyVersion() async throws -> UpdateConfirmedPrivacyVersionRequest.Response { 18 | try await makeRequest(request: UpdateConfirmedPrivacyVersionRequest()) 19 | } 20 | 21 | func updateConfirmedTermsVersion() async throws -> UpdateConfirmedTermsVersionRequest.Response { 22 | try await makeRequest(request: UpdateConfirmedTermsVersionRequest()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /NativeAppTemplate/Networking/Services/PermissionsService.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Razeware LLC 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // Notwithstanding the foregoing, you may not use, copy, modify, merge, publish, 14 | // distribute, sublicense, create a derivative work, and/or sell copies of the 15 | // Software in any work that is designed, intended, or marketed for pedagogical or 16 | // instructional purposes related to programming, coding, application development, 17 | // or information technology. Permission for such use, copying, modification, 18 | // merger, publication, distribution, sublicensing, creation of derivative works, 19 | // or sale is expressly withheld. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | // THE SOFTWARE. 28 | 29 | import class Foundation.URLSession 30 | 31 | struct PermissionsService: Service { 32 | let networkClient: NativeAppTemplateAPI 33 | let session = URLSession(configuration: .default) 34 | } 35 | 36 | extension PermissionsService { 37 | func allPermissions() async throws -> PermissionsRequest.Response { 38 | try await makeRequest(request: PermissionsRequest()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /NativeAppTemplate/Networking/Services/ShopsService.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Razeware LLC 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // Notwithstanding the foregoing, you may not use, copy, modify, merge, publish, 14 | // distribute, sublicense, create a derivative work, and/or sell copies of the 15 | // Software in any work that is designed, intended, or marketed for pedagogical or 16 | // instructional purposes related to programming, coding, application development, 17 | // or information technology. Permission for such use, copying, modification, 18 | // merger, publication, distribution, sublicensing, creation of derivative works, 19 | // or sale is expressly withheld. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | // THE SOFTWARE. 28 | 29 | import class Foundation.URLSession 30 | 31 | struct ShopsService: Service { 32 | let networkClient: NativeAppTemplateAPI 33 | let session = URLSession(configuration: .default) 34 | } 35 | 36 | // MARK: - Internal 37 | extension ShopsService { 38 | func allShops() async throws -> GetShopsRequest.Response { 39 | try await makeRequest(request: GetShopsRequest()) 40 | } 41 | 42 | func updateShop(id: String, shop: Shop) async throws -> UpdateShopRequest.Response { 43 | let request = UpdateShopRequest(id: id, shop: shop) 44 | return try await makeRequest(request: request) 45 | } 46 | 47 | func destroyShop(id: String) async throws -> DestroyShopRequest.Response { 48 | try await makeRequest(request: DestroyShopRequest(id: id)) 49 | } 50 | 51 | func shopDetail(id: String) async throws -> GetShopDetailRequest.Response { 52 | try await makeRequest(request: GetShopDetailRequest(id: id)) 53 | } 54 | 55 | func makeShop(shop: Shop) async throws -> MakeShopRequest.Response { 56 | let request = MakeShopRequest(shop: shop) 57 | return try await makeRequest(request: request) 58 | } 59 | 60 | func resetShop(id: String) async throws -> ResetShopRequest.Response { 61 | try await makeRequest(request: ResetShopRequest(id: id)) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /NativeAppTemplate/Persistence/KeychainStore/KeychainStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainStore.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2020/04/12. 6 | // Copyright © 2024 Daisuke Adachi All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import KeychainAccess 11 | 12 | enum KeychainStoreError: Error { 13 | case secCallFailed(Error) 14 | case notFound 15 | case badData 16 | case archiveFailure(Error) 17 | } 18 | 19 | protocol KeychainStore { 20 | associatedtype DataType: NSObject, NSCoding 21 | 22 | var account: String { get set } 23 | var service: String { get set } 24 | 25 | func remove() throws 26 | func retrieve() throws -> DataType 27 | func store(_ data: DataType) throws 28 | } 29 | 30 | extension KeychainStore { 31 | func remove() throws { 32 | let keychain = Keychain(service: service) 33 | 34 | do { 35 | try keychain.remove(account) 36 | } catch { 37 | throw KeychainStoreError.secCallFailed(error) 38 | } 39 | } 40 | 41 | func retrieve() throws -> DataType { 42 | let keychain = Keychain(service: service) 43 | let archived: Data? 44 | 45 | archived = try? keychain.getData(account) 46 | 47 | guard archived != nil else { 48 | throw KeychainStoreError.notFound 49 | } 50 | 51 | do { 52 | guard 53 | let unarchived = try NSKeyedUnarchiver.unarchivedObject(ofClass: DataType.self, from: archived!) 54 | else { 55 | throw KeychainStoreError.badData 56 | } 57 | 58 | return unarchived 59 | } catch { 60 | throw KeychainStoreError.archiveFailure(error) 61 | } 62 | } 63 | 64 | func store(_ data: DataType) throws { 65 | let archived: Data 66 | print("data: \(data)") 67 | do { 68 | archived = try NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: true) 69 | } catch { 70 | throw KeychainStoreError.archiveFailure(error) 71 | } 72 | 73 | let keychain = Keychain(service: service) 74 | 75 | do { 76 | try keychain.set(archived, key: account) 77 | } catch { 78 | throw KeychainStoreError.secCallFailed(error) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /NativeAppTemplate/Persistence/KeychainStore/LoggedInShopkeeperKeychainStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoggedInShopkeeperStore.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2021/01/17. 6 | // 7 | 8 | import Foundation 9 | 10 | struct LoggedInShopkeeperKeychainStore: KeychainStore { 11 | // Make sure the account name doesn't match the bundle identifier! 12 | var account = String.keychainAccountLoggedInShopkeeper 13 | var service = String.keychainServiceLoggedInShopkeeper 14 | 15 | typealias DataType = LoggedInShopkeeper 16 | } 17 | -------------------------------------------------------------------------------- /NativeAppTemplate/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NativeAppTemplate/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryUserDefaults 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | CA92.1 13 | 14 | 15 | 16 | NSPrivacyTracking 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /NativeAppTemplate/Sessions/SessionController+States.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Razeware LLC 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // Notwithstanding the foregoing, you may not use, copy, modify, merge, publish, 14 | // distribute, sublicense, create a derivative work, and/or sell copies of the 15 | // Software in any work that is designed, intended, or marketed for pedagogical or 16 | // instructional purposes related to programming, coding, application development, 17 | // or information technology. Permission for such use, copying, modification, 18 | // merger, publication, distribution, sublicensing, creation of derivative works, 19 | // or sale is expressly withheld. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | // THE SOFTWARE. 28 | 29 | import struct Foundation.Date 30 | 31 | extension SessionController { 32 | enum UserState: Sendable { 33 | case loggedIn 34 | case loggingIn 35 | case notLoggedIn 36 | } 37 | 38 | enum SessionState: Sendable { 39 | case unknown 40 | case online 41 | case offline 42 | } 43 | 44 | enum PermissionState: Equatable, Sendable { 45 | case notLoaded 46 | case loading 47 | case loaded 48 | case error 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /NativeAppTemplate/Sessions/Shopkeeper+Backdoor.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Razeware LLC 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // Notwithstanding the foregoing, you may not use, copy, modify, merge, publish, 14 | // distribute, sublicense, create a derivative work, and/or sell copies of the 15 | // Software in any work that is designed, intended, or marketed for pedagogical or 16 | // instructional purposes related to programming, coding, application development, 17 | // or information technology. Permission for such use, copying, modification, 18 | // merger, publication, distribution, sublicensing, creation of derivative works, 19 | // or sale is expressly withheld. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | // THE SOFTWARE. 28 | 29 | import class Foundation.UserDefaults 30 | 31 | extension Shopkeeper { 32 | static var backdoor: Shopkeeper? { 33 | guard let backdoorToken = UserDefaults.standard.string(forKey: "shopkeeperBackdoorToken") else { return nil } 34 | 35 | let shopkeeperDict = [ 36 | "id": "BACKDOOR_SHOPKEEPER", 37 | "email": "shopkeeper@nativeapptemplate.com", 38 | "name": "BACKDOORSHOPKEEPER", 39 | "uid": "uid", 40 | "token": backdoorToken, 41 | "client": "client", 42 | "expiry": "123456789" 43 | ] 44 | 45 | return Shopkeeper(dictionary: shopkeeperDict) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /NativeAppTemplate/Styleguide/Color+Extensions.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Razeware LLC 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // Notwithstanding the foregoing, you may not use, copy, modify, merge, publish, 14 | // distribute, sublicense, create a derivative work, and/or sell copies of the 15 | // Software in any work that is designed, intended, or marketed for pedagogical or 16 | // instructional purposes related to programming, coding, application development, 17 | // or information technology. Permission for such use, copying, modification, 18 | // merger, publication, distribution, sublicensing, creation of derivative works, 19 | // or sale is expressly withheld. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | // THE SOFTWARE. 28 | 29 | import SwiftUI 30 | 31 | extension Color { 32 | static var backgroundColor: Color { 33 | Color("backgroundColor") 34 | } 35 | 36 | static var snackError: Color { 37 | Color("error") 38 | } 39 | 40 | static var snackWarning: Color { 41 | Color("warning") 42 | } 43 | 44 | static var snackSuccess: Color { 45 | Color("success") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /NativeAppTemplate/Styleguide/Inter/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Styleguide/Inter/Inter-Bold.ttf -------------------------------------------------------------------------------- /NativeAppTemplate/Styleguide/Inter/Inter-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/NativeAppTemplate/Styleguide/Inter/Inter-Medium.ttf -------------------------------------------------------------------------------- /NativeAppTemplate/Styleguide/UIColor+Extensions.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Razeware LLC 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // Notwithstanding the foregoing, you may not use, copy, modify, merge, publish, 14 | // distribute, sublicense, create a derivative work, and/or sell copies of the 15 | // Software in any work that is designed, intended, or marketed for pedagogical or 16 | // instructional purposes related to programming, coding, application development, 17 | // or information technology. Permission for such use, copying, modification, 18 | // merger, publication, distribution, sublicensing, creation of derivative works, 19 | // or sale is expressly withheld. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | // THE SOFTWARE. 28 | 29 | import UIKit 30 | 31 | extension UIColor { 32 | static var backgroundUiColor: UIColor { 33 | UIColor(named: "backgroundColor")! 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /NativeAppTemplate/Styleguide/UIFont+Extensions.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Razeware LLC 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // Notwithstanding the foregoing, you may not use, copy, modify, merge, publish, 14 | // distribute, sublicense, create a derivative work, and/or sell copies of the 15 | // Software in any work that is designed, intended, or marketed for pedagogical or 16 | // instructional purposes related to programming, coding, application development, 17 | // or information technology. Permission for such use, copying, modification, 18 | // merger, publication, distribution, sublicensing, creation of derivative works, 19 | // or sale is expressly withheld. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | // THE SOFTWARE. 28 | 29 | import UIKit 30 | 31 | extension UIFont { 32 | static var uiLargeTitle: UIFont { 33 | .init(name: "Inter-Bold", size: 36.0)! 34 | } 35 | static var uiHeadline: UIFont { 36 | .init(name: "Inter-Medium", size: 18.0)! 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/App Root/AcceptPrivacyView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AcceptPrivacyView.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/12/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AcceptPrivacyView: View { 11 | @Environment(\.dismiss) private var dismiss 12 | @Environment(MessageBus.self) private var messageBus 13 | @Environment(SessionController.self) private var sessionController 14 | @Binding var arePrivacyAccepted: Bool 15 | @State private var isUpdating = false 16 | 17 | var body: some View { 18 | contentView 19 | } 20 | } 21 | 22 | // MARK: - private 23 | private extension AcceptPrivacyView { 24 | var contentView: some View { 25 | 26 | @ViewBuilder var contentView: some View { 27 | if isUpdating { 28 | LoadingView() 29 | } else { 30 | acceptPrivacyView 31 | } 32 | } 33 | 34 | return contentView 35 | } 36 | 37 | var acceptPrivacyView: some View { 38 | VStack { 39 | let agreement = "Please accept updated [\(String.privacyPolicy)](\(String.privacyPolicyUrl))." 40 | Text(.init(agreement)) 41 | .padding(.top, 48) 42 | 43 | MainButtonView(title: String.accept, type: .primary(withArrow: false)) { 44 | updateConfirmedPrivacyVersion() 45 | } 46 | .padding(24) 47 | 48 | Spacer() 49 | } 50 | .navigationTitle(String.privacyPolicyUpdated) 51 | .navigationBarTitleDisplayMode(.inline) 52 | } 53 | 54 | private func updateConfirmedPrivacyVersion() { 55 | Task { @MainActor in 56 | do { 57 | isUpdating = true 58 | try await sessionController.updateConfirmedPrivacyVersion() 59 | messageBus.post(message: Message(level: .success, message: .confirmedPrivacyVersionUpdated)) 60 | } catch { 61 | messageBus.post(message: Message(level: .error, message: "\(String.confirmedPrivacyVersionUpdatedError) \(error.localizedDescription)", autoDismiss: false)) 62 | } 63 | 64 | arePrivacyAccepted = true 65 | dismiss() 66 | } 67 | } 68 | } 69 | 70 | #Preview { 71 | @Previewable @State var arePrivacyAccepted = true 72 | 73 | return AcceptPrivacyView(arePrivacyAccepted: $arePrivacyAccepted) 74 | } 75 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/App Root/AcceptTermsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AcceptTermsView.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/12/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AcceptTermsView: View { 11 | @Environment(\.dismiss) private var dismiss 12 | @Environment(MessageBus.self) private var messageBus 13 | @Environment(SessionController.self) private var sessionController 14 | @Binding var areTermsAccepted: Bool 15 | @State private var isUpdating = false 16 | 17 | var body: some View { 18 | contentView 19 | } 20 | } 21 | 22 | // MARK: - private 23 | private extension AcceptTermsView { 24 | var contentView: some View { 25 | 26 | @ViewBuilder var contentView: some View { 27 | if isUpdating { 28 | LoadingView() 29 | } else { 30 | acceptTermsView 31 | } 32 | } 33 | 34 | return contentView 35 | } 36 | 37 | var acceptTermsView: some View { 38 | VStack { 39 | let agreement = "Please accept updated [\(String.termsOfUse)](\(String.termsOfUseUrl))." 40 | Text(.init(agreement)) 41 | .padding(.top, 48) 42 | 43 | MainButtonView(title: String.accept, type: .primary(withArrow: false)) { 44 | updateConfirmedTermsVersion() 45 | } 46 | .padding(24) 47 | 48 | Spacer() 49 | } 50 | .navigationTitle(String.termsOfUseUpdated) 51 | .navigationBarTitleDisplayMode(.inline) 52 | } 53 | 54 | private func updateConfirmedTermsVersion() { 55 | Task { @MainActor in 56 | do { 57 | isUpdating = true 58 | try await sessionController.updateConfirmedTermsVersion() 59 | messageBus.post(message: Message(level: .success, message: .confirmedTermsVersionUpdated)) 60 | } catch { 61 | messageBus.post(message: Message(level: .error, message: "\(String.confirmedTermsVersionUpdatedError) \(error.localizedDescription)", autoDismiss: false)) 62 | } 63 | 64 | areTermsAccepted = true 65 | dismiss() 66 | } 67 | } 68 | } 69 | 70 | #Preview { 71 | @Previewable @State var areTermsAccepted = true 72 | 73 | return AcceptTermsView(areTermsAccepted: $areTermsAccepted) 74 | } 75 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/App Root/ForgotPasswordView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForgotPasswordView.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/03/02. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ForgotPasswordView: View { 11 | @Environment(\.dismiss) private var dismiss 12 | @Environment(MessageBus.self) private var messageBus 13 | @State var email: String = "" 14 | @State private var isSendingResetPasswordInstructions = false 15 | let signUpRepository: SignUpRepository 16 | 17 | init( 18 | signUpRepository: SignUpRepository 19 | ) { 20 | self.signUpRepository = signUpRepository 21 | } 22 | 23 | private var hasInvalidData: Bool { 24 | if Utility.isBlank(email) { 25 | return true 26 | } 27 | 28 | if !Utility.validateEmail(email) { 29 | return true 30 | } 31 | 32 | return false 33 | } 34 | } 35 | 36 | extension ForgotPasswordView { 37 | var body: some View { 38 | contentView 39 | } 40 | } 41 | 42 | // MARK: - private 43 | private extension ForgotPasswordView { 44 | var contentView: some View { 45 | 46 | @ViewBuilder var contentView: some View { 47 | if isSendingResetPasswordInstructions { 48 | LoadingView() 49 | } else { 50 | forgotPasswordView 51 | } 52 | } 53 | 54 | return contentView 55 | } 56 | 57 | var forgotPasswordView: some View { 58 | Form { 59 | Section { 60 | TextField(String.placeholderEmail, text: $email) 61 | .textContentType(.emailAddress) 62 | .autocapitalization(.none) 63 | } header: { 64 | Text(String.email) 65 | } footer: { 66 | if Utility.isBlank(email) { 67 | Text(String.emailIsRequired) 68 | .foregroundStyle(.red) 69 | } else if !Utility.validateEmail(email) { 70 | Text(String.emailIsInvalid) 71 | .foregroundStyle(.red) 72 | } 73 | } 74 | 75 | MainButtonView(title: String.buttonSendMeResetPasswordInstructions, type: .primary(withArrow: false)) { 76 | sendMeResetPasswordInstructionsTapped() 77 | } 78 | .disabled(hasInvalidData) 79 | .listRowBackground(Color.clear) 80 | } 81 | .navigationTitle(String.forgotYourPassword) 82 | } 83 | 84 | private func sendMeResetPasswordInstructionsTapped() { 85 | let whitespacesAndNewlines = CharacterSet.whitespacesAndNewlines 86 | let theEmail = email.trimmingCharacters(in: whitespacesAndNewlines) 87 | 88 | Task { @MainActor in 89 | do { 90 | let sendResetPassword = SendResetPassword(email: theEmail) 91 | try await signUpRepository.sendResetPasswordInstruction(sendResetPassword: sendResetPassword) 92 | messageBus.post(message: Message(level: .success, message: .sentResetPasswordInstruction, autoDismiss: false)) 93 | dismiss() 94 | } catch { 95 | UIApplication.dismissKeyboard() 96 | messageBus.post(message: Message(level: .error, message: String.sentResetPasswordInstructionError, autoDismiss: false)) 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/App Root/MessageBarView.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Razeware LLC 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // Notwithstanding the foregoing, you may not use, copy, modify, merge, publish, 14 | // distribute, sublicense, create a derivative work, and/or sell copies of the 15 | // Software in any work that is designed, intended, or marketed for pedagogical or 16 | // instructional purposes related to programming, coding, application development, 17 | // or information technology. Permission for such use, copying, modification, 18 | // merger, publication, distribution, sublicensing, creation of derivative works, 19 | // or sale is expressly withheld. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | // THE SOFTWARE. 28 | 29 | import SwiftUI 30 | 31 | extension AnyTransition { 32 | static var moveAndFade: AnyTransition { 33 | AnyTransition.move(edge: .bottom) 34 | .combined(with: .opacity) 35 | } 36 | } 37 | 38 | struct MessageBarView: View { 39 | @Bindable var messageBus: MessageBus 40 | 41 | var body: some View { 42 | VStack { 43 | if messageBus.messageVisible { 44 | SnackbarView( 45 | state: messageBus.currentMessage!.snackbarState, 46 | visible: $messageBus.messageVisible 47 | ) 48 | } 49 | } 50 | .transition(.moveAndFade) 51 | .animation(.default, value: messageBus.messageVisible) 52 | } 53 | } 54 | 55 | struct MessageBarView_Previews: PreviewProvider { 56 | static var previews: some View { 57 | let messageBus = MessageBus() 58 | messageBus.post(message: Message(level: .warning, message: "This is a warning")) 59 | 60 | return VStack { 61 | Button(action: { 62 | messageBus.messageVisible.toggle() 63 | }) { 64 | Text(verbatim: "Show/Hide") 65 | } 66 | 67 | Button(action: { 68 | messageBus.post(message: Message(level: .success, message: "Button clicked!")) 69 | }) { 70 | Text(verbatim: "Post new message") 71 | } 72 | 73 | MessageBarView(messageBus: messageBus) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/App Root/PermissionsLoadingView.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Razeware LLC 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // Notwithstanding the foregoing, you may not use, copy, modify, merge, publish, 14 | // distribute, sublicense, create a derivative work, and/or sell copies of the 15 | // Software in any work that is designed, intended, or marketed for pedagogical or 16 | // instructional purposes related to programming, coding, application development, 17 | // or information technology. Permission for such use, copying, modification, 18 | // merger, publication, distribution, sublicensing, creation of derivative works, 19 | // or sale is expressly withheld. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | // THE SOFTWARE. 28 | 29 | import SwiftUI 30 | 31 | struct PermissionsLoadingView: View { 32 | @Environment(SessionController.self) private var sessionController 33 | @State private var isShowingLogoutAlert = false 34 | 35 | var body: some View { 36 | LoadingView() 37 | .onTapGesture(count: 5) { 38 | isShowingLogoutAlert.toggle() 39 | } 40 | .alert( 41 | String.forceSignOut, 42 | isPresented: $isShowingLogoutAlert 43 | ) { 44 | Button(role: .destructive) { 45 | logout() 46 | } label: { 47 | Text(String.signOut) 48 | } 49 | } 50 | } 51 | 52 | func logout() { 53 | Task { 54 | try await sessionController.logout() 55 | } 56 | } 57 | } 58 | 59 | struct PermissionsLoadingView_Previews: PreviewProvider { 60 | static var previews: some View { 61 | PermissionsLoadingView() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/App Root/ResendConfirmationInstructionsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResendConfirmationInstructionsView.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/09/30. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ResendConfirmationInstructionsView: View { 11 | @Environment(\.dismiss) private var dismiss 12 | @Environment(MessageBus.self) private var messageBus 13 | @State var email: String = "" 14 | @State private var isSendingConfirmationInstructions = false 15 | let signUpRepository: SignUpRepository 16 | 17 | init( 18 | signUpRepository: SignUpRepository 19 | ) { 20 | self.signUpRepository = signUpRepository 21 | } 22 | 23 | private var hasInvalidData: Bool { 24 | if Utility.isBlank(email) { 25 | return true 26 | } 27 | 28 | if !Utility.validateEmail(email) { 29 | return true 30 | } 31 | 32 | return false 33 | } 34 | } 35 | 36 | extension ResendConfirmationInstructionsView { 37 | var body: some View { 38 | contentView 39 | } 40 | } 41 | 42 | // MARK: - private 43 | private extension ResendConfirmationInstructionsView { 44 | var contentView: some View { 45 | 46 | @ViewBuilder var contentView: some View { 47 | if isSendingConfirmationInstructions { 48 | LoadingView() 49 | } else { 50 | resendConfirmationInstructionsView 51 | } 52 | } 53 | 54 | return contentView 55 | } 56 | 57 | var resendConfirmationInstructionsView: some View { 58 | Form { 59 | Section { 60 | TextField(String.placeholderEmail, text: $email) 61 | .textContentType(.emailAddress) 62 | .autocapitalization(.none) 63 | } header: { 64 | Text(String.email) 65 | } footer: { 66 | if Utility.isBlank(email) { 67 | Text(String.emailIsRequired) 68 | .foregroundStyle(.red) 69 | } else if !Utility.validateEmail(email) { 70 | Text(String.emailIsInvalid) 71 | .foregroundStyle(.red) 72 | } 73 | } 74 | 75 | MainButtonView(title: String.buttonSendMeConfirmationInstructions, type: .primary(withArrow: false)) { 76 | sendMeConfirmationInstructionsTapped() 77 | } 78 | .disabled(hasInvalidData) 79 | .listRowBackground(Color.clear) 80 | } 81 | .navigationTitle(String.didntReceiveConfirmationInstructions) 82 | } 83 | 84 | private func sendMeConfirmationInstructionsTapped() { 85 | let whitespacesAndNewlines = CharacterSet.whitespacesAndNewlines 86 | let theEmail = email.trimmingCharacters(in: whitespacesAndNewlines) 87 | 88 | Task { @MainActor in 89 | do { 90 | let sendConfirmation = SendConfirmation(email: theEmail) 91 | try await signUpRepository.sendConfirmationInstruction(sendConfirmation: sendConfirmation) 92 | messageBus.post(message: Message(level: .success, message: .sentConfirmationInstruction, autoDismiss: false)) 93 | dismiss() 94 | } catch { 95 | UIApplication.dismissKeyboard() 96 | messageBus.post(message: Message(level: .error, message: String.sentConfirmationInstructionError, autoDismiss: false)) 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/App Root/SignUpOrSignInView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpOrSignInView.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2024/01/16. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SignUpOrSignInView: View { 11 | var body: some View { 12 | contentView 13 | } 14 | } 15 | 16 | // MARK: - private 17 | private extension SignUpOrSignInView { 18 | var contentView: some View { 19 | @ViewBuilder var contentView: some View { 20 | ScrollView { 21 | VStack { 22 | Image("logo") 23 | .resizable() 24 | .aspectRatio(contentMode: .fit) 25 | .frame(width: 384, height: 24) 26 | .padding() 27 | 28 | Image("onboarding1Slim") 29 | .resizable() 30 | .aspectRatio(contentMode: .fit) 31 | .frame(height: 256) 32 | .padding() 33 | 34 | let agreement = "By signing up or signing in, you agree to the [\(String.termsOfUse)](\(String.termsOfUseUrl)) and [\(String.privacyPolicy)](\(String.privacyPolicyUrl))." 35 | Text(.init(agreement)) 36 | .padding(.top, 16) 37 | .padding(.horizontal, 24) 38 | 39 | VStack { 40 | NavigationLink(destination: SignUpView(signUpRepository: SignUpRepository())) { 41 | MainButtonImageView(title: String.signUpForAnAccount, type: .primary(withArrow: false)) 42 | .padding(.top, 8) 43 | .padding(.horizontal, 24) 44 | } 45 | 46 | Text(verbatim: "or") 47 | .padding(.top, 8) 48 | 49 | NavigationLink(destination: SignInEmailAndPasswordView(signUpRepository: SignUpRepository())) { 50 | Text(String.signInToYourAccount) 51 | .font(.uiLabel) 52 | } 53 | .padding(.top, 8) 54 | } 55 | .padding(.top, 4) 56 | 57 | Spacer() 58 | } 59 | .padding(.bottom) 60 | } 61 | .navigationBarTitleDisplayMode(.inline) 62 | .toolbar { 63 | ToolbarItem(placement: .navigationBarTrailing) { 64 | Link(String.supportWebsite, destination: URL(string: String.supportWebsiteUrl)!) 65 | } 66 | } 67 | .background(Color.backgroundColor) 68 | } 69 | 70 | return contentView 71 | } 72 | } 73 | 74 | #Preview { 75 | SignUpOrSignInView() 76 | } 77 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/Empty States/LoadingView.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Razeware LLC 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // Notwithstanding the foregoing, you may not use, copy, modify, merge, publish, 14 | // distribute, sublicense, create a derivative work, and/or sell copies of the 15 | // Software in any work that is designed, intended, or marketed for pedagogical or 16 | // instructional purposes related to programming, coding, application development, 17 | // or information technology. Permission for such use, copying, modification, 18 | // merger, publication, distribution, sublicensing, creation of derivative works, 19 | // or sale is expressly withheld. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | // THE SOFTWARE. 28 | 29 | import SwiftUI 30 | 31 | struct LoadingView: View { 32 | var body: some View { 33 | VStack { 34 | ProgressView().scaleEffect(1.0, anchor: .center) 35 | .padding([.bottom], 12) 36 | Text(String.loading) 37 | .font(.uiHeadline) 38 | } 39 | } 40 | } 41 | 42 | struct LoadingView_Previews: PreviewProvider { 43 | static var previews: some View { 44 | LoadingView().inAllColorSchemes 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/Empty States/NeedAppUpdatesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NeedAppUpdatesView.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/12/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NeedAppUpdatesView: View { 11 | @Environment(\.openURL) var openURL 12 | 13 | var body: some View { 14 | VStack { 15 | Image(systemName: "exclamationmark.arrow.circlepath") 16 | .resizable() 17 | .aspectRatio(contentMode: .fit) 18 | .frame(width: 96) 19 | .foregroundStyle(.titleText) 20 | .padding() 21 | Text(String.updateApp) 22 | .font(.uiTitle1) 23 | .foregroundStyle(.titleText) 24 | .padding(.top) 25 | Text(String.installNewVersionApp) 26 | .foregroundStyle(.contentText) 27 | .padding(.top, 4) 28 | Button { 29 | openURL(URL(string: String.appStoreUrl)!) 30 | } label: { 31 | Text(String.updateApp) 32 | } 33 | .padding(.top) 34 | } 35 | .frame(maxWidth: .infinity, maxHeight: .infinity) 36 | .background(Color.backgroundColor) 37 | .edgesIgnoringSafeArea(.all) 38 | } 39 | } 40 | 41 | #Preview { 42 | NeedAppUpdatesView() 43 | } 44 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/Empty States/OfflineView.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Razeware LLC 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // Notwithstanding the foregoing, you may not use, copy, modify, merge, publish, 14 | // distribute, sublicense, create a derivative work, and/or sell copies of the 15 | // Software in any work that is designed, intended, or marketed for pedagogical or 16 | // instructional purposes related to programming, coding, application development, 17 | // or information technology. Permission for such use, copying, modification, 18 | // merger, publication, distribution, sublicensing, creation of derivative works, 19 | // or sale is expressly withheld. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | // THE SOFTWARE. 28 | 29 | import SwiftUI 30 | 31 | struct OfflineView: View { 32 | var body: some View { 33 | VStack { 34 | Image(systemName: "wifi.slash") 35 | .resizable() 36 | .aspectRatio(contentMode: .fit) 37 | .frame(width: 96) 38 | .padding() 39 | .foregroundStyle(.titleText) 40 | 41 | Text(String.noConnection) 42 | .font(.uiTitle1) 43 | .foregroundStyle(.titleText) 44 | .multilineTextAlignment(.center) 45 | .padding(.top) 46 | 47 | Text(String.checkInternetConnection) 48 | .font(.uiLabel) 49 | .lineSpacing(8) 50 | .foregroundStyle(.contentText) 51 | .multilineTextAlignment(.center) 52 | .padding(.top, 4) 53 | .padding(.horizontal, 32) 54 | } 55 | .frame(maxWidth: .infinity, maxHeight: .infinity) 56 | .background(Color.backgroundColor) 57 | .edgesIgnoringSafeArea(.all) 58 | } 59 | } 60 | 61 | struct OfflineView_Previews: PreviewProvider { 62 | static var previews: some View { 63 | OfflineView() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/Scan/CompleteScanResultView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompleteScanResultView.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/04. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CompleteScanResultView: View { 11 | @Environment(\.dismiss) private var dismiss 12 | @Environment(MessageBus.self) private var messageBus 13 | var completeScanResult: CompleteScanResult 14 | 15 | init( 16 | completeScanResult: CompleteScanResult 17 | ) { 18 | self.completeScanResult = completeScanResult 19 | } 20 | 21 | var body: some View { 22 | contentView 23 | } 24 | } 25 | 26 | // MARK: - private 27 | private extension CompleteScanResultView { 28 | var contentView: some View { 29 | 30 | @ViewBuilder var contentView: some View { 31 | switch completeScanResult.type { 32 | case .completed, .reset: 33 | succeededView 34 | case .failed: 35 | failedView 36 | case .idled: 37 | idledView 38 | } 39 | } 40 | 41 | return contentView 42 | } 43 | 44 | @ViewBuilder var succeededView: some View { 45 | if let itemTag = completeScanResult.itemTag { 46 | GroupBox(label: Label(String("Result"), systemImage: "checkmark.circle") ) { 47 | Text(String(itemTag.queueNumber)) 48 | .font(.uiTitle1) 49 | 50 | if itemTag.state == .completed { 51 | CompletedTag() 52 | } else { 53 | IdlingTag() 54 | } 55 | 56 | if completeScanResult.type == .reset { 57 | Text(completeScanResult.type.displayString) 58 | } 59 | 60 | HStack(alignment: .firstTextBaseline) { 61 | Text(completeScanResult.scannedAt.cardTimeAgoInWordsDateString) 62 | .font(.uiBodyCustom) 63 | .foregroundStyle(.successSecondaryForeground) 64 | Text(verbatim: "complete scanned") 65 | .font(.uiFootnote) 66 | .foregroundStyle(.successSecondaryForeground) 67 | } 68 | .padding(.top, 8) 69 | } 70 | .backgroundStyle(.successBackground) 71 | } 72 | } 73 | 74 | @ViewBuilder var failedView: some View { 75 | GroupBox(label: Label(String("Error"), systemImage: "exclamationmark.triangle") ) { 76 | Text(completeScanResult.message) 77 | } 78 | .backgroundStyle(.failureBackground) 79 | } 80 | 81 | @ViewBuilder var idledView: some View { 82 | GroupBox(label: Label(String("Result"), systemImage: "checkmark.circle") ) { 83 | } 84 | .backgroundStyle(.successBackground) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/Shared/Tags/CompletedTag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompletedTag.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/04. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CompletedTag: View { 11 | var body: some View { 12 | TagView( 13 | text: "completed", 14 | textColor: .completedTagForeground, 15 | backgroundColor: .completedTagBackground, 16 | borderColor: .completedTagBorder 17 | ) 18 | } 19 | } 20 | 21 | struct CompletedTag_Previews: PreviewProvider { 22 | static var previews: some View { 23 | VStack(spacing: 12) { 24 | completedTag.colorScheme(.light) 25 | completedTag.colorScheme(.dark) 26 | } 27 | } 28 | 29 | static var completedTag: some View { 30 | CompletedTag() 31 | .padding() 32 | .background(Color.backgroundColor) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/Shared/Tags/CustomerScannedTag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomerScannedTag.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/04. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CustomerScannedTag: View { 11 | var body: some View { 12 | TagView( 13 | text: "customer scanned", 14 | textColor: .customerScannedTagForeground, 15 | backgroundColor: .customerScannedTagBackground, 16 | borderColor: .customerScannedTagBorder 17 | ) 18 | } 19 | } 20 | 21 | struct CustomerScannedTag_Previews: PreviewProvider { 22 | static var previews: some View { 23 | VStack(spacing: 12) { 24 | customerScannedTag.colorScheme(.light) 25 | customerScannedTag.colorScheme(.dark) 26 | } 27 | } 28 | 29 | static var customerScannedTag: some View { 30 | CustomerScannedTag() 31 | .padding() 32 | .background(Color.backgroundColor) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/Shared/Tags/IdlingTagView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IdlingTag.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/04. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct IdlingTag: View { 11 | var body: some View { 12 | TagView( 13 | text: "idling", 14 | textColor: .idlingTagForeground, 15 | backgroundColor: .idlingTagBackground, 16 | borderColor: .idlingTagBorder 17 | ) 18 | } 19 | } 20 | 21 | struct IdlingTag_Previews: PreviewProvider { 22 | static var previews: some View { 23 | VStack(spacing: 12) { 24 | idlingTag.colorScheme(.light) 25 | idlingTag.colorScheme(.dark) 26 | } 27 | } 28 | 29 | static var idlingTag: some View { 30 | IdlingTag() 31 | .padding() 32 | .background(Color.backgroundColor) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/Shared/Tags/TagView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TagView.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/01/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TagView: View { 11 | private static let defaultIconHeight: CGFloat = 12.0 12 | 13 | private struct SizeKey: PreferenceKey { 14 | static func reduce(value: inout CGSize?, nextValue: () -> CGSize?) { 15 | value = value ?? nextValue() 16 | } 17 | } 18 | 19 | @State private var height: CGFloat? 20 | 21 | let text: String 22 | let textColor: Color 23 | let backgroundColor: Color 24 | let borderColor: Color 25 | var image: Image? 26 | 27 | var body: some View { 28 | HStack(spacing: 4) { 29 | image? 30 | .resizable() 31 | .aspectRatio(contentMode: .fit) 32 | .foregroundStyle(textColor) 33 | .frame(height: Self.defaultIconHeight) 34 | 35 | Text(text.uppercased()) 36 | .foregroundStyle(textColor) 37 | .font(.uiUppercaseTag) 38 | .kerning(0.5) 39 | .background( 40 | GeometryReader { proxy in 41 | Color.clear.preference(key: SizeKey.self, value: proxy.size) 42 | } 43 | ) 44 | } 45 | .padding([.vertical], 4) 46 | .padding([.horizontal], 8) 47 | .background(backgroundColor) 48 | .cornerRadius(4) // This is a bit hacky. 49 | .onPreferenceChange(SizeKey.self) { size in 50 | Task { @MainActor in 51 | height = size?.height 52 | } 53 | } 54 | } 55 | } 56 | 57 | struct TagView_Previews: PreviewProvider { 58 | static var previews: some View { 59 | VStack(spacing: 18) { 60 | TagView( 61 | text: "this is a tag", 62 | textColor: .white, 63 | backgroundColor: .red, 64 | borderColor: .yellow 65 | ) 66 | 67 | TagView( 68 | text: "with an image", 69 | textColor: .white, 70 | backgroundColor: .red, 71 | borderColor: .yellow, 72 | image: Image(systemName: "checkmark") 73 | ) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/Shop Detail/ShopDetailCardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShopDetailCardView.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/04. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ShopDetailCardView: View { 11 | let itemTag: ItemTag 12 | 13 | init( 14 | itemTag: ItemTag 15 | ) { 16 | self.itemTag = itemTag 17 | } 18 | 19 | var body: some View { 20 | content 21 | } 22 | 23 | var content: some View { 24 | HStack { 25 | Text(String(itemTag.queueNumber)) 26 | .font(.uiTitle4) 27 | 28 | Spacer() 29 | 30 | VStack(alignment: .trailing) { 31 | if itemTag.scanState == ScanState.scanned { 32 | CustomerScannedTag() 33 | 34 | if let customerReadAt = itemTag.customerReadAt { 35 | Text(customerReadAt.cardTimeString) 36 | .font(.uiFootnote) 37 | .foregroundStyle(.contentText) 38 | } 39 | } 40 | } 41 | 42 | Spacer() 43 | 44 | VStack(alignment: .trailing) { 45 | if itemTag.state == .completed { 46 | CompletedTag() 47 | 48 | if let completedAt = itemTag.completedAt { 49 | Text(completedAt.cardTimeString) 50 | .font(.uiFootnote) 51 | .foregroundStyle(.contentText) 52 | } 53 | } else { 54 | IdlingTag() 55 | } 56 | } 57 | .frame(minWidth: 82, alignment: .trailing) 58 | } 59 | .frame(minHeight: 48) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/Shop List/ItemTag List/ItemTagListCardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemTagListCardView.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/04. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ItemTagListCardView: View { 11 | let itemTag: ItemTag 12 | 13 | var body: some View { 14 | Text(String(itemTag.queueNumber)) 15 | .font(.uiTitle4) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/Shop List/ShopListCardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShopListCardView.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/02/05. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ShopListCardView: View { 11 | let shop: Shop 12 | 13 | var body: some View { 14 | VStack(alignment: .leading) { 15 | Text(shop.name) 16 | .font(.uiTitle4) 17 | .foregroundStyle(.accent) 18 | 19 | let statImageSize = 12.0 20 | 21 | Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 12, verticalSpacing: 4) { 22 | GridRow { 23 | Image(systemName: "person.2") 24 | .frame(width: statImageSize, height: statImageSize) 25 | .foregroundStyle(.secondaryText) 26 | Text(String(shop.scannedItemTagsCount)) 27 | .font(.uiLabelBold) 28 | .gridColumnAlignment(.trailing) 29 | Text(verbatim: "tags scanned by customers") 30 | .font(.uiFootnote) 31 | .foregroundStyle(.contentText) 32 | } 33 | 34 | GridRow { 35 | Image(systemName: "flag.checkered") 36 | .frame(width: statImageSize, height: statImageSize) 37 | .foregroundStyle(.secondaryText) 38 | Text(String(shop.completedItemTagsCount)) 39 | .font(.uiLabelBold) 40 | Text(verbatim: "completed tags") 41 | .font(.uiFootnote) 42 | .foregroundStyle(.contentText) 43 | } 44 | 45 | GridRow { 46 | Image(systemName: "rectangle.stack") 47 | .frame(width: statImageSize, height: statImageSize) 48 | .foregroundStyle(.secondaryText) 49 | Text(String(shop.itemTagsCount)) 50 | .font(.uiLabelBold) 51 | Text(verbatim: "all tags") 52 | .font(.uiFootnote) 53 | .foregroundStyle(.contentText) 54 | } 55 | } 56 | .padding(.top) 57 | 58 | Text(shop.description) 59 | .font(.uiCaption) 60 | .foregroundStyle(.contentText) 61 | .padding(.top) 62 | } 63 | .padding() 64 | .dynamicTypeSize(...DynamicTypeSize.accessibility1) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/Shop Settings/NumberTagsWebpageListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumberTagsWebpageList.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/04. 6 | // 7 | 8 | import SwiftUI 9 | import UniformTypeIdentifiers 10 | 11 | enum NumberTagsWebpageListType: String, Identifiable, CaseIterable, Codable, Hashable { 12 | case server 13 | 14 | var id: Self { self } 15 | 16 | var displayString: String { 17 | switch self { 18 | case .server: 19 | return String.serverNumberTagsWebpage 20 | } 21 | } 22 | } 23 | 24 | struct NumberTagsWebpageListView: View { 25 | @Environment(MessageBus.self) private var messageBus 26 | private var shop: Shop 27 | 28 | init( 29 | shop: Shop 30 | ) { 31 | self.shop = shop 32 | } 33 | } 34 | 35 | // MARK: - View 36 | extension NumberTagsWebpageListView { 37 | var body: some View { 38 | contentView 39 | } 40 | } 41 | 42 | // MARK: - private 43 | private extension NumberTagsWebpageListView { 44 | var contentView: some View { 45 | 46 | @ViewBuilder var contentView: some View { 47 | numberTagsWebpageListView 48 | } 49 | 50 | return contentView 51 | } 52 | 53 | var numberTagsWebpageListView: some View { 54 | VStack { 55 | Text(shop.name) 56 | .font(.uiTitle1) 57 | .foregroundStyle(.titleText) 58 | .padding(.top, 24) 59 | List(NumberTagsWebpageListType.allCases) { numberTagsWebpageListType in 60 | switch numberTagsWebpageListType { 61 | case .server: 62 | Section { 63 | Link(numberTagsWebpageListType.displayString, destination: shop.displayShopServerUrl) 64 | } header: { 65 | Label(String("Server"), systemImage: "storefront") 66 | } footer: { 67 | Button(String.copyWebpageUrl) { 68 | copyWebpageUrl(shop.displayShopServerUrl.absoluteString) 69 | } 70 | } 71 | .listRowBackground(Color.cardBackground) 72 | } 73 | } 74 | } 75 | .navigationTitle(String.shopSettingsNumberTagsWebpageLabel) 76 | } 77 | 78 | func copyWebpageUrl(_ url: String) { 79 | UIPasteboard.general.setValue(url, forPasteboardType: UTType.plainText.identifier) 80 | messageBus.post(message: Message(level: .success, message: .webpageUrlCopied)) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /NativeAppTemplate/UI/UIKit/MailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MailView.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2023/11/12. 6 | // 7 | 8 | import AVFoundation 9 | import Foundation 10 | import MessageUI 11 | import SwiftUI 12 | import UIKit 13 | 14 | struct MailView: UIViewControllerRepresentable { 15 | @Environment(\.presentationMode) var presentation 16 | @Binding var result: Result? 17 | var recipients = [String]() 18 | var subject = "" 19 | var messageBody = "" 20 | var isHTML = false 21 | 22 | class Coordinator: NSObject, MFMailComposeViewControllerDelegate { 23 | @Binding var presentation: PresentationMode 24 | @Binding var result: Result? 25 | 26 | init(presentation: Binding, 27 | result: Binding?>) { 28 | _presentation = presentation 29 | _result = result 30 | } 31 | 32 | func mailComposeController(_: MFMailComposeViewController, 33 | didFinishWith result: MFMailComposeResult, 34 | error: Error?) { 35 | defer { 36 | $presentation.wrappedValue.dismiss() 37 | } 38 | guard error == nil else { 39 | self.result = .failure(error!) 40 | return 41 | } 42 | self.result = .success(result) 43 | 44 | if result == .sent { 45 | AudioServicesPlayAlertSound(SystemSoundID(1001)) 46 | } 47 | } 48 | } 49 | 50 | func makeCoordinator() -> Coordinator { 51 | Coordinator(presentation: presentation, 52 | result: $result) 53 | } 54 | 55 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> MFMailComposeViewController { 56 | let mfMailComposeViewController = MFMailComposeViewController() 57 | mfMailComposeViewController.setToRecipients(recipients) 58 | mfMailComposeViewController.setSubject(subject) 59 | mfMailComposeViewController.setMessageBody(messageBody, isHTML: isHTML) 60 | mfMailComposeViewController.mailComposeDelegate = context.coordinator 61 | return mfMailComposeViewController 62 | } 63 | 64 | func updateUIViewController(_: MFMailComposeViewController, 65 | context _: UIViewControllerRepresentableContext) {} 66 | } 67 | -------------------------------------------------------------------------------- /NativeAppTemplate/Utilities/ImageSaver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageSaver.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/04. 6 | // 7 | 8 | import UIKit 9 | 10 | class ImageSaver: NSObject { 11 | private var completion: (_ error: Error?) -> Void = { _ in } 12 | 13 | func save(image: UIImage, completion: @escaping (_ error: Error?) -> Void) { 14 | self.completion = completion 15 | UIImageWriteToSavedPhotosAlbum(image, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil) 16 | } 17 | 18 | @objc 19 | private func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) { 20 | completion(error) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /NativeAppTemplate/Utilities/QRCodeGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QRCodeGenerator.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/04. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct QRCodeGenerator { 11 | func generate(inputText: String, scale: CGFloat = 2, centerImage: UIImage?) -> UIImage? { 12 | guard let qrFilter = CIFilter(name: "CIQRCodeGenerator") 13 | else { return nil } 14 | 15 | let inputData = inputText.data(using: .utf8) 16 | qrFilter.setValue(inputData, forKey: "inputMessage") 17 | qrFilter.setValue("H", forKey: "inputCorrectionLevel") 18 | 19 | guard let ciImage = qrFilter.outputImage 20 | else { return nil } 21 | 22 | let sizeTransform = CGAffineTransform(scaleX: scale, y: scale) 23 | let scaledCiImage = ciImage.transformed(by: sizeTransform) 24 | 25 | let context = CIContext() 26 | guard let cgImage = context.createCGImage(scaledCiImage, from: scaledCiImage.extent) 27 | else { return nil } 28 | 29 | if let centerImage = centerImage { 30 | return UIImage(cgImage: cgImage).composited(withSmallCenterImage: centerImage) 31 | } else { 32 | return UIImage(cgImage: cgImage) 33 | } 34 | } 35 | 36 | func generateWithCenterText(inputText: String, scale: CGFloat = 2, centerText: String) -> UIImage? { 37 | if let centerImage = centerText.image( 38 | withAttributes: [ 39 | .font: UIFont.systemFont(ofSize: 40.0), 40 | .backgroundColor: UIColor.white 41 | ] 42 | ) { 43 | return generate(inputText: inputText, scale: scale, centerImage: centerImage) 44 | } else { 45 | return generate(inputText: inputText, scale: scale, centerImage: nil) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /NativeAppTemplateTests/Models/ShopkeeperTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShopkeeperTest.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/01/31. 6 | // 7 | 8 | import Testing 9 | import SwiftyJSON 10 | @testable import NativeAppTemplate 11 | 12 | struct ShopkeeperTest { 13 | let shopkeeperDictionary = [ 14 | "id": "5712F2DF-DFC7-A3AA-66BC-191203654A1A", 15 | "account_id": "5712F2DF-DFC7-A3AA-66BC-191203654A1Z", 16 | "personal_account_id": "5712F2DF-DFC7-A3AA-66BC-191203654A1Z", 17 | "account_owner_id": "5712F2DF-DFC7-A3AA-66BC-191203654A1C", 18 | "account_name": "Account1", 19 | "email": "email@example.com", 20 | "name": "Jhon Smith", 21 | "time_zone": "Tokyo", 22 | "uid": "email@example.com", 23 | "token": "Sample.Token", 24 | "client": "Sample.Client", 25 | "expiry": "123456789" 26 | ] 27 | 28 | @Test func shopkeeperCorrectlyPopulatesWithDictionary() { 29 | guard let shopkeeper = Shopkeeper(dictionary: shopkeeperDictionary) else { 30 | Issue.record("Shopkeeper should be correctly populated") 31 | return 32 | } 33 | 34 | #expect(shopkeeperDictionary["id"] == shopkeeper.id) 35 | #expect(shopkeeperDictionary["account_id"] == shopkeeper.accountId) 36 | #expect(shopkeeperDictionary["personal_account_id"] == shopkeeper.personalAccountId) 37 | #expect(shopkeeperDictionary["account_owner_id"] == shopkeeper.accountOwnerId) 38 | #expect(shopkeeperDictionary["account_name"] == shopkeeper.accountName) 39 | #expect(shopkeeperDictionary["email"] == shopkeeper.email) 40 | #expect(shopkeeperDictionary["name"] == shopkeeper.name) 41 | #expect(shopkeeperDictionary["time_zone"] == shopkeeper.timeZone) 42 | #expect(shopkeeperDictionary["uid"] == shopkeeper.uid) 43 | #expect(shopkeeperDictionary["token"] == shopkeeper.token) 44 | #expect(shopkeeperDictionary["client"] == shopkeeper.client) 45 | #expect(shopkeeperDictionary["expiry"] == shopkeeper.expiry) 46 | } 47 | 48 | func shopkeeperDictionaryHasRequiredFields() { 49 | var invalidDictionary = shopkeeperDictionary 50 | invalidDictionary.removeValue(forKey: "id") 51 | let shopkeeper = Shopkeeper(dictionary: invalidDictionary) 52 | 53 | #expect(shopkeeper == nil) 54 | } 55 | 56 | func additionalEntriesInTheDictionaryAreIgnored() { 57 | var overSpecifiedDictionary = shopkeeperDictionary 58 | overSpecifiedDictionary["extra_field"] = "some-guff" 59 | let shopkeeper = Shopkeeper(dictionary: overSpecifiedDictionary) 60 | 61 | #expect(shopkeeper != nil) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /NativeAppTemplateTests/Networking/Adapters/ItemTagAdapterTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemTagAdapterTest.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/03/01. 6 | // 7 | 8 | import Testing 9 | import SwiftyJSON 10 | @testable import NativeAppTemplate 11 | 12 | struct ItemTagAdapterTest { 13 | let sampleResource: JSON = [ 14 | "id": "5712F2DF-DFC7-A3AA-66BC-191203654A1A", 15 | "type": "item_tag", 16 | "attributes": [ 17 | "shop_id": "88705252-2FD2-4414-9E85-E6888033294A", 18 | "queue_number": "A001", 19 | "state": "idled", 20 | "scan_state": "unscanned", 21 | "created_at": "2020-01-01T12:00:00.000Z", 22 | "shop_name": "Shop1", 23 | "customer_read_at": "2020-01-02T12:00:00.000Z", 24 | "completed_at": "2020-01-04T12:00:00.000Z", 25 | "already_completed": false 26 | ] 27 | ] 28 | 29 | func makeJsonAPIResource(for dict: JSON) throws -> JSONAPIResource { 30 | let json: JSON = [ 31 | "data": [ 32 | dict 33 | ] 34 | ] 35 | 36 | let document = JSONAPIDocument(json) 37 | return document.data.first! 38 | } 39 | 40 | @Test func validResourceProcessedCorrectly() async throws { 41 | let resource = try makeJsonAPIResource(for: sampleResource) 42 | let itemTag = try ItemTagAdapter.process(resource: resource) 43 | #expect("5712F2DF-DFC7-A3AA-66BC-191203654A1A" == itemTag.id) 44 | } 45 | 46 | @Test func inInvalidTypeThrows() throws { 47 | var sample = sampleResource 48 | sample["type"] = "invalid" 49 | 50 | let resource = try makeJsonAPIResource(for: sample) 51 | 52 | #expect { try ItemTagAdapter.process(resource: resource) } throws: { error in 53 | let entityAdapterError = error as? EntityAdapterError 54 | return EntityAdapterError.invalidResourceTypeForAdapter == entityAdapterError 55 | } 56 | } 57 | 58 | @Test func missingnAccountIdThrows() throws { 59 | var sample = sampleResource 60 | sample["attributes"].dictionaryObject?.removeValue(forKey: "shop_id") 61 | 62 | let resource = try makeJsonAPIResource(for: sample) 63 | 64 | #expect { try ItemTagAdapter.process(resource: resource) } throws: { error in 65 | let entityAdapterError = error as? EntityAdapterError 66 | return EntityAdapterError.invalidOrMissingAttributes == entityAdapterError 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /NativeAppTemplateTests/Networking/Adapters/ShopAdapterTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShopAdapterTest.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/01/31. 6 | // 7 | 8 | import Testing 9 | import SwiftyJSON 10 | @testable import NativeAppTemplate 11 | 12 | struct ShopAdapterTest { 13 | let sampleResource: JSON = [ 14 | "id": "5712F2DF-DFC7-A3AA-66BC-191203654A1C", 15 | "type": "shop", 16 | "attributes": [ 17 | "name": "Shop1", 18 | "description": "This is a Shop1", 19 | "time_zone": "Tokyo", 20 | "display_shop_server_path": "https://api.nativeapptemplate.com/display/shops/1ed7ea32-65d5-4e64-97a0-0e00b6cee8c3?type=server", // swiftlint:disable:this line_length 21 | "item_tags_count": 10, 22 | "scanned_item_tags_count": 1, 23 | "completed_item_tags_count": 2 24 | ], 25 | "relationships": [ 26 | "account": [ 27 | "data": [ 28 | "id": "96C3444D-5B64-1EFF-2354-55787BD43277", 29 | "type": "Account1", 30 | "attributes": [ 31 | "name": "Shop1", 32 | "owner_id": "88705252-2FD2-4414-9E85-E6888033294B", 33 | "personal": true, 34 | "is_admin": true, 35 | "owner_name": "Jhon Smith", 36 | "accounts_shopkeepers_count": 99, 37 | "accounts_invitations_count": 98, 38 | "shops_count": 96 39 | ] 40 | ] 41 | ] 42 | ], 43 | "meta": [ 44 | "limit_count": 96, 45 | "created_shops_count": 3 46 | ] 47 | ] 48 | 49 | func makeJsonAPIResource(for dict: JSON) throws -> JSONAPIResource { 50 | let json: JSON = [ 51 | "data": [ 52 | dict 53 | ] 54 | ] 55 | 56 | let document = JSONAPIDocument(json) 57 | return document.data.first! 58 | } 59 | 60 | @Test func validResourceProcessedCorrectly() async throws { 61 | let resource = try makeJsonAPIResource(for: sampleResource) 62 | let shop = try ShopAdapter.process(resource: resource) 63 | #expect("5712F2DF-DFC7-A3AA-66BC-191203654A1C" == shop.id) 64 | } 65 | 66 | @Test func inInvalidTypeThrows() throws { 67 | var sample = sampleResource 68 | sample["type"] = "invalid" 69 | 70 | let resource = try makeJsonAPIResource(for: sample) 71 | 72 | #expect { try ShopAdapter.process(resource: resource) } throws: { error in 73 | let entityAdapterError = error as? EntityAdapterError 74 | return EntityAdapterError.invalidResourceTypeForAdapter == entityAdapterError 75 | } 76 | } 77 | 78 | @Test func missingnNameThrows() throws { 79 | var sample = sampleResource 80 | sample["attributes"].dictionaryObject?.removeValue(forKey: "name") 81 | 82 | let resource = try makeJsonAPIResource(for: sample) 83 | 84 | #expect { try ShopAdapter.process(resource: resource) } throws: { error in 85 | let entityAdapterError = error as? EntityAdapterError 86 | return EntityAdapterError.invalidOrMissingAttributes == entityAdapterError 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /NativeAppTemplateTests/Networking/Adapters/ShopkeeperAdapterTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShopkeeperAdapterTest.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/01/31. 6 | // 7 | 8 | import Testing 9 | import SwiftyJSON 10 | @testable import NativeAppTemplate 11 | 12 | struct ShopkeeperAdapterTest { 13 | let sampleResource: JSON = [ 14 | "id": "5712F2DF-DFC7-A3AA-66BC-191203654A1C", 15 | "type": "shopkeeper", 16 | "attributes": [ 17 | "name": "Shopkeeper1", 18 | "email": "email@example.com", 19 | "time_zone": "Tokyo" 20 | ] 21 | ] 22 | 23 | func makeJsonAPIResource(for dict: JSON) throws -> JSONAPIResource { 24 | let json: JSON = [ 25 | "data": [ 26 | dict 27 | ] 28 | ] 29 | 30 | let document = JSONAPIDocument(json) 31 | return document.data.first! 32 | } 33 | 34 | @Test func validResourceProcessedCorrectly() async throws { 35 | let resource = try makeJsonAPIResource(for: sampleResource) 36 | let shopkeeper = try ShopkeeperAdapter.process(resource: resource) 37 | #expect("5712F2DF-DFC7-A3AA-66BC-191203654A1C" == shopkeeper.id) 38 | } 39 | 40 | @Test func inInvalidTypeThrows() throws { 41 | var sample = sampleResource 42 | sample["type"] = "invalid" 43 | 44 | let resource = try makeJsonAPIResource(for: sample) 45 | 46 | #expect { try ShopkeeperAdapter.process(resource: resource) } throws: { error in 47 | let entityAdapterError = error as? EntityAdapterError 48 | return EntityAdapterError.invalidResourceTypeForAdapter == entityAdapterError 49 | } 50 | } 51 | 52 | @Test func missingnNameThrows() throws { 53 | var sample = sampleResource 54 | sample["attributes"].dictionaryObject?.removeValue(forKey: "name") 55 | 56 | let resource = try makeJsonAPIResource(for: sample) 57 | 58 | #expect { try ShopkeeperAdapter.process(resource: resource) } throws: { error in 59 | let entityAdapterError = error as? EntityAdapterError 60 | return EntityAdapterError.invalidOrMissingAttributes == entityAdapterError 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /NativeAppTemplateTests/Networking/Adapters/ShopkeeperSignInAdapterTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShopkeeperSignInAdapterTest.swift 3 | // NativeAppTemplate 4 | // 5 | // Created by Daisuke Adachi on 2025/01/31. 6 | // 7 | 8 | import Testing 9 | import SwiftyJSON 10 | @testable import NativeAppTemplate 11 | 12 | struct ShopkeeperSignInAdapterTest { 13 | let sampleResource: JSON = [ 14 | "id": "5712F2DF-DFC7-A3AA-66BC-191203654A1C", 15 | "type": "shopkeeper_sign_in", 16 | "attributes": [ 17 | "account_id": "5712F2DF-DFC7-A3AA-66BC-191203654A1Z", 18 | "personal_account_id": "5712F2DF-DFC7-A3AA-66BC-191203654A1Z", 19 | "account_owner_id": "5712F2DF-DFC7-A3AA-66BC-191203654A1C", 20 | "account_name": "Account1", 21 | "email": "email@example.com", 22 | "name": "Jhon Smith", 23 | "time_zone": "Tokyo", 24 | "uid": "email@example.com" 25 | ] 26 | ] 27 | 28 | func makeJsonAPIResource(for dict: JSON) throws -> JSONAPIResource { 29 | let json: JSON = [ 30 | "data": [ 31 | dict 32 | ] 33 | ] 34 | 35 | let document = JSONAPIDocument(json) 36 | return document.data.first! 37 | } 38 | 39 | @Test func validResourceProcessedCorrectly() async throws { 40 | let resource = try makeJsonAPIResource(for: sampleResource) 41 | let shopkeeper = try ShopkeeperSignInAdapter.process(resource: resource) 42 | #expect("5712F2DF-DFC7-A3AA-66BC-191203654A1C" == shopkeeper.id) 43 | } 44 | 45 | @Test func inInvalidTypeThrows() throws { 46 | var sample = sampleResource 47 | sample["type"] = "invalid" 48 | 49 | let resource = try makeJsonAPIResource(for: sample) 50 | 51 | #expect { try ShopkeeperSignInAdapter.process(resource: resource) } throws: { error in 52 | let entityAdapterError = error as? EntityAdapterError 53 | return EntityAdapterError.invalidResourceTypeForAdapter == entityAdapterError 54 | } 55 | } 56 | 57 | @Test func missingnNameThrows() throws { 58 | var sample = sampleResource 59 | sample["attributes"].dictionaryObject?.removeValue(forKey: "name") 60 | 61 | let resource = try makeJsonAPIResource(for: sample) 62 | 63 | #expect { try ShopkeeperSignInAdapter.process(resource: resource) } throws: { error in 64 | let entityAdapterError = error as? EntityAdapterError 65 | return EntityAdapterError.invalidOrMissingAttributes == entityAdapterError 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If you come across a security vulnerability in __nativeapptemplate__, or any of the __nativeapptemplate__.com infrastructure that it connects to, please do not file an 6 | issue on GitHub. 7 | 8 | Instead, please document your issue as fully as you can, and email your issue report directly to support@nativeapptemplate.com. 9 | 10 | We take security very seriously, and will assess any reports first before we embark upon getting them fixed as soon as possible. 11 | 12 | Thanks! 13 | -------------------------------------------------------------------------------- /SampleCode.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // See the LICENSE.txt file for this sample’s licensing information. 3 | // 4 | // SampleCode.xcconfig 5 | // 6 | 7 | // The `SAMPLE_CODE_DISAMBIGUATOR` configuration is to make it easier to build 8 | // and run a sample code project. Once you set your project's development team, 9 | // you'll have a unique bundle identifier. This is because the bundle identifier 10 | // is derived based on the 'SAMPLE_CODE_DISAMBIGUATOR' value. Do not use this 11 | // approach in your own projects—it's only useful for sample code projects because 12 | // they are frequently downloaded and don't have a development team set. 13 | SAMPLE_CODE_DISAMBIGUATOR=${DEVELOPMENT_TEAM} 14 | -------------------------------------------------------------------------------- /docs/images/nfc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/docs/images/nfc.gif -------------------------------------------------------------------------------- /docs/images/organization.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/docs/images/organization.gif -------------------------------------------------------------------------------- /docs/images/overview_after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/docs/images/overview_after.png -------------------------------------------------------------------------------- /docs/images/overview_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/docs/images/overview_before.png -------------------------------------------------------------------------------- /docs/images/screenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/docs/images/screenshots.png -------------------------------------------------------------------------------- /docs/images/screenshots_nfc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeapptemplate/NativeAppTemplate-Free-iOS/b36b646b27b4bace1960942c79fb4e59edb74da8/docs/images/screenshots_nfc.png --------------------------------------------------------------------------------