├── .circleci
└── config.yml
├── .gitattributes
├── .gitignore
├── .ruby-version
├── .swift-version
├── CHANGELOG.yml
├── Gemfile
├── Gemfile.lock
├── Kiosk.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── Kiosk.xccheckout
└── xcshareddata
│ └── xcschemes
│ └── Kiosk.xcscheme
├── Kiosk.xcworkspace
└── contents.xcworkspacedata
├── Kiosk
├── Admin
│ ├── AdminCardTestingViewController.swift
│ ├── AdminLogViewController.swift
│ ├── AdminPanelViewController.swift
│ ├── AuctionWebViewController.swift
│ ├── ChooseAuctionViewController.swift
│ └── PasswordAlertViewController.swift
├── App
│ ├── APIPingManager.swift
│ ├── AppDelegate+GlobalActions.swift
│ ├── AppDelegate.swift
│ ├── AppSetup.swift
│ ├── AppViewController.swift
│ ├── BidderDetailsRetrieval.swift
│ ├── CardHandler.swift
│ ├── Constants.swift
│ ├── CreditCardValidation.h
│ ├── CreditCardValidation.m
│ ├── GlobalFunctions.swift
│ ├── KioskDateFormatter.h
│ ├── KioskDateFormatter.m
│ ├── Logger.swift
│ ├── MarkdownParser.swift
│ ├── Models
│ │ ├── Artist.swift
│ │ ├── Artwork.swift
│ │ ├── Bid.swift
│ │ ├── Bidder.swift
│ │ ├── BidderPosition.swift
│ │ ├── BuyersPremium.swift
│ │ ├── Card.swift
│ │ ├── CreditCard.swift
│ │ ├── GenericError.swift
│ │ ├── Image.swift
│ │ ├── JSONAble.swift
│ │ ├── Location.swift
│ │ ├── Sale.swift
│ │ ├── SaleArtwork.swift
│ │ ├── SaleArtworkViewModel.swift
│ │ ├── SystemTime.swift
│ │ └── User.swift
│ ├── NSErrorExtensions.swift
│ ├── Networking
│ │ ├── APIKeys.swift
│ │ ├── ArtsyAPI.swift
│ │ ├── NetworkLogger.swift
│ │ ├── Networking.swift
│ │ ├── UserCredentials.swift
│ │ └── XAppToken.swift
│ ├── OfflineViewController.swift
│ ├── StubResponses.h
│ ├── StubResponses.m
│ ├── SwiftExtensions.swift
│ ├── UIStoryboardSegueExtensions.swift
│ ├── UIViewControllerExtensions.swift
│ ├── UIViewSubclassesErrorExtensions.swift
│ └── Views
│ │ ├── Button Subclasses
│ │ └── Button.swift
│ │ ├── RegisterFlowView.swift
│ │ ├── SimulatorOnlyView.swift
│ │ ├── Spinner.swift
│ │ ├── SwitchView.swift
│ │ └── Text Fields
│ │ ├── CursorView.swift
│ │ └── TextField.swift
├── Auction Listings
│ ├── ListingsCountdownManager.swift
│ ├── ListingsViewController.swift
│ ├── ListingsViewModel.swift
│ ├── MasonryCollectionViewCell.swift
│ ├── TableCollectionViewCell.swift
│ └── WebViewController.swift
├── Bid Fulfillment
│ ├── AdminCCBypassNetworkModel.swift
│ ├── BidCheckingNetworkModel.swift
│ ├── BidDetailsPreviewView.swift
│ ├── BidderNetworkModel.swift
│ ├── ConfirmYourBidArtsyLoginViewController.swift
│ ├── ConfirmYourBidEnterYourEmailViewController.swift
│ ├── ConfirmYourBidPINViewController.swift
│ ├── ConfirmYourBidPasswordViewController.swift
│ ├── ConfirmYourBidViewController.swift
│ ├── FulfillmentContainerViewController.swift
│ ├── FulfillmentNavigationController.swift
│ ├── GenericFormValidationViewModel.swift
│ ├── KeypadContainerView.swift
│ ├── KeypadView.swift
│ ├── KeypadViewModel.swift
│ ├── KeypadViewPreviewIB.png
│ ├── LoadingViewController.swift
│ ├── LoadingViewModel.swift
│ ├── ManualCreditCardInputViewController.swift
│ ├── ManualCreditCardInputViewModel.swift
│ ├── Models
│ │ ├── BidDetails.swift
│ │ ├── NewUser.swift
│ │ └── RegistrationCoordinator.swift
│ ├── PlaceBidNetworkModel.swift
│ ├── PlaceBidViewController.swift
│ ├── RegisterViewController.swift
│ ├── RegistrationEmailViewController.swift
│ ├── RegistrationMobileViewController.swift
│ ├── RegistrationPasswordViewController.swift
│ ├── RegistrationPasswordViewModel.swift
│ ├── RegistrationPostalZipViewController.swift
│ ├── StripeManager.swift
│ ├── SwipeCreditCardViewController.swift
│ ├── YourBiddingDetailsViewController.swift
│ ├── edit_button@2x.png
│ └── toolbar_close@2x.png
├── Help
│ ├── HelpAnimator.swift
│ └── HelpViewController.swift
├── HelperFunctions.swift
├── Images.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── folioicon152-1.png
│ │ ├── folioicon152.png
│ │ ├── folioicon76.png
│ │ └── iTunesArtwork@2x.png
│ ├── ArtsyLogo.imageset
│ │ ├── Contents.json
│ │ └── Logo@2x.png
│ ├── BackButtonBackground.imageset
│ │ ├── BackButtonBackground@2x.png
│ │ └── Contents.json
│ ├── BidHighestBidder.imageset
│ │ ├── BidHighestBidder@2x.png
│ │ └── Contents.json
│ ├── BidNotHighestBidder.imageset
│ │ ├── BidNotHighestBidder@2x.png
│ │ └── Contents.json
│ ├── CardSwipe.imageset
│ │ ├── Contents.json
│ │ └── card-swipe-example.jpg
│ ├── Contents.json
│ ├── LaunchImage.launchimage
│ │ └── Contents.json
│ ├── ProductionFlag.imageset
│ │ ├── Contents.json
│ │ └── ProductionFlag@2x.png
│ ├── StagingFlag.imageset
│ │ ├── Contents.json
│ │ └── StagingFlag@2x.png
│ ├── StubbingFlag.imageset
│ │ ├── Contents.json
│ │ └── StagingFlag@2x.png
│ └── xbtn_white.imageset
│ │ ├── Contents.json
│ │ ├── xbtn_white.png
│ │ └── xbtn_white@2x.png
├── ListingsCollectionViewCell.swift
├── Observable+JSONAble.swift
├── Observable+Logging.swift
├── Observable+Operators.swift
├── Sale Artwork Details
│ ├── ImageTiledDataSource.swift
│ ├── SaleArtworkDetailsViewController.swift
│ ├── SaleArtworkZoomViewController.swift
│ └── WhitespaceGobbler.swift
├── Storyboards
│ ├── Auction.storyboard
│ ├── Fulfillment.storyboard
│ ├── KeypadView.xib
│ ├── StoryboardIdentifiers.swift
│ └── UIStoryboardExtensions.swift
├── Stubbed Responses
│ ├── ActiveAuctions.json
│ ├── Artist.json
│ ├── Artwork.json
│ ├── AuctionInfo.json
│ ├── AuctionInfoForArtwork.json
│ ├── AuctionListings.json
│ ├── Auctions.json
│ ├── CreateABid.json
│ ├── CreateABidFail.json
│ ├── CreatePINForBidder.json
│ ├── FindBidder.json
│ ├── FindMyBidderRegistration.json
│ ├── ForgotPassword.json
│ ├── Me.json
│ ├── MyBidPosition.json
│ ├── MyBidPositionsForAuctionArtwork.json
│ ├── MyBiddersForAuction.json
│ ├── MyCreditCards.json
│ ├── Ping.json
│ ├── RegisterCard.json
│ ├── RegisterToBid.json
│ ├── SystemTime.json
│ ├── XApp.json
│ └── XAuth.json
├── Supporting Files
│ ├── BridgingHeader.h
│ ├── Info.plist
│ └── PodsBridgingHeader.h
├── UIKit+Rx.swift
├── UILabel+Fonts.swift
├── UIView+LongPressDisplayMessage.swift
└── UIViewController+Bidding.swift
├── KioskTests
├── App
│ ├── ArtsyProviderTests.swift
│ ├── LoggerTests.swift
│ └── SwiftExtensionsTests.swift
├── AppViewControllerTests.swift
├── ArtsyAPISpec.swift
├── Bid Fulfillment
│ ├── AdminCCBypassNetworkModelTests.swift
│ ├── BidderNetworkModelTests.swift
│ ├── ConfirmYourBidArtsyLoginViewControllerTests.swift
│ ├── ConfirmYourBidEnterYourEmailViewControllerTests.swift
│ ├── ConfirmYourBidPINViewControllerTests.swift
│ ├── ConfirmYourBidViewControllerTests.swift
│ ├── FulfillmentContainerViewControllerTests.swift
│ ├── FulfillmentNavigationControllerTests.swift
│ ├── GenericFormValidationViewModelTests.swift
│ ├── KeypadViewModelTests.swift
│ ├── LoadingViewControllerTests.swift
│ ├── LoadingViewModelTests.swift
│ ├── ManualCreditCardInputViewControllerTests.swift
│ ├── ManualCreditCardInputViewModelTests.swift
│ ├── PlaceBidNetworkModelTests.swift
│ ├── PlaceBidViewControllerTests.swift
│ ├── RegistrationEmailViewControllerTests.swift
│ ├── RegistrationMobileViewControllerTests.swift
│ ├── RegistrationPasswordViewControllerTests.swift
│ ├── RegistrationPasswordViewModelTests.swift
│ ├── RegistrationPostalZipViewControllerTests.swift
│ ├── StripeManagerTests.swift
│ ├── SwipeCreditCardViewControllerTests.swift
│ └── YourBiddingDetailsViewControllerTests.swift
├── CardHandlerTests.swift
├── HelpViewControllerTests.swift
├── Info.plist
├── KioskTests.swift
├── ListingsViewControllerTests.swift
├── ListingsViewModelTests.swift
├── Models
│ ├── ArtistTests.swift
│ ├── ArtworkTests.swift
│ ├── BidTests.swift
│ ├── BidderPositionTests.swift
│ ├── BidderTests.swift
│ ├── ImageTests.swift
│ ├── SaleArtworkTests.swift
│ ├── SaleTests.swift
│ └── SystemTimeTests.swift
├── ReferenceImages
│ ├── AppViewControllerTests
│ │ └── looks_right_offline@2x.png
│ ├── ConfirmYourBidArtsyLoginViewControllerTests
│ │ └── looks_right_by_default@2x.png
│ ├── ConfirmYourBidEnterYourEmailViewControllerTests
│ │ └── looks_right_by_default@2x.png
│ ├── ConfirmYourBidPINViewControllerTests
│ │ └── looks_right_by_default@2x.png
│ ├── HelpViewControllerTests
│ │ ├── a_help_view_controller__looks_correct@2x.png
│ │ └── with_a_buyers_premium__a_help_view_controller__looks_correct@2x.png
│ ├── ListingsViewControllerTests
│ │ ├── when_displaying_stubbed_contents__with_artworks_not_for_sale__a_listings_controller__alphabetical@2x.png
│ │ ├── when_displaying_stubbed_contents__with_artworks_not_for_sale__a_listings_controller__grid@2x.png
│ │ ├── when_displaying_stubbed_contents__with_artworks_not_for_sale__a_listings_controller__highest_bid@2x.png
│ │ ├── when_displaying_stubbed_contents__with_artworks_not_for_sale__a_listings_controller__least_bids@2x.png
│ │ ├── when_displaying_stubbed_contents__with_artworks_not_for_sale__a_listings_controller__lowest_bid@2x.png
│ │ ├── when_displaying_stubbed_contents__with_artworks_not_for_sale__a_listings_controller__most_bids@2x.png
│ │ ├── when_displaying_stubbed_contents__with_lot_numbers__a_listings_controller__alphabetical@2x.png
│ │ ├── when_displaying_stubbed_contents__with_lot_numbers__a_listings_controller__grid@2x.png
│ │ ├── when_displaying_stubbed_contents__with_lot_numbers__a_listings_controller__highest_bid@2x.png
│ │ ├── when_displaying_stubbed_contents__with_lot_numbers__a_listings_controller__least_bids@2x.png
│ │ ├── when_displaying_stubbed_contents__with_lot_numbers__a_listings_controller__lowest_bid@2x.png
│ │ ├── when_displaying_stubbed_contents__with_lot_numbers__a_listings_controller__most_bids@2x.png
│ │ ├── when_displaying_stubbed_contents__without_lot_numbers__a_listings_controller__alphabetical@2x.png
│ │ ├── when_displaying_stubbed_contents__without_lot_numbers__a_listings_controller__grid@2x.png
│ │ ├── when_displaying_stubbed_contents__without_lot_numbers__a_listings_controller__highest_bid@2x.png
│ │ ├── when_displaying_stubbed_contents__without_lot_numbers__a_listings_controller__least_bids@2x.png
│ │ ├── when_displaying_stubbed_contents__without_lot_numbers__a_listings_controller__lowest_bid@2x.png
│ │ └── when_displaying_stubbed_contents__without_lot_numbers__a_listings_controller__most_bids@2x.png
│ ├── LoadingViewControllerTests
│ │ ├── default__placing_a_bid@2x.png
│ │ ├── default__registering_a_user@2x.png
│ │ ├── ending__placing_bid_error_due_to_outbid@2x.png
│ │ ├── ending__placing_bid_succeeded_but_not_resolved@2x.png
│ │ ├── ending__placing_bid_success_highest@2x.png
│ │ ├── ending__placing_bid_success_not_highest@2x.png
│ │ ├── ending__registering_user_not_resolved@2x.png
│ │ ├── ending__registering_user_success@2x.png
│ │ ├── errors__correctly_placing_a_bid@2x.png
│ │ └── errors__correctly_registering_a_user@2x.png
│ ├── ManualCreditCardInputViewControllerTests
│ │ ├── after_CC_is_entered_with_valid_dates__uses_registerButtonCommand_enabledness_for_date_button@2x.png
│ │ ├── asks_for_CC_number_by_default@2x.png
│ │ ├── enables_CC_entry_field_when_CC_is_valid@2x.png
│ │ └── shows_errors@2x.png
│ ├── PlaceBidViewControllerTests
│ │ ├── looks_right_by_default@2x.png
│ │ ├── looks_right_with_a_custom_saleArtwork@2x.png
│ │ ├── with_bids__a_bid_view_controller_view_controller__looks_correct@2x.png
│ │ ├── with_bids__a_bid_view_controller_view_controller__with_lot_number__looks_correct@2x.png
│ │ ├── with_bids__a_bid_view_controller_view_controller__without_lot_number__looks_correct@2x.png
│ │ ├── with_no_bids__a_bid_view_controller_view_controller__looks_correct@2x.png
│ │ ├── with_no_bids__a_bid_view_controller_view_controller__with_lot_number__looks_correct@2x.png
│ │ ├── with_no_bids__a_bid_view_controller_view_controller__without_lot_number__looks_correct@2x.png
│ │ ├── with_no_bids__with_a_buyers_premium__a_bid_view_controller_view_controller__with_lot_number__looks_correct@2x.png
│ │ └── with_no_bids__with_a_buyers_premium__a_bid_view_controller_view_controller__without_lot_number__looks_correct@2x.png
│ ├── RegisterFlowViewTests
│ │ ├── not_requiring_zip_code__a_register_flow_view__handles_full_data@2x.png
│ │ ├── not_requiring_zip_code__a_register_flow_view__handles_highlighted_index@2x.png
│ │ ├── not_requiring_zip_code__a_register_flow_view__handles_partial_data@2x.png
│ │ ├── not_requiring_zip_code__a_register_flow_view__looks_right_by_default@2x.png
│ │ ├── requiring_zip_code__a_register_flow_view__handles_full_data@2x.png
│ │ ├── requiring_zip_code__a_register_flow_view__handles_highlighted_index@2x.png
│ │ ├── requiring_zip_code__a_register_flow_view__handles_partial_data@2x.png
│ │ └── requiring_zip_code__a_register_flow_view__looks_right_by_default@2x.png
│ ├── RegistrationEmailViewControllerTests
│ │ ├── looks_right_by_default@2x.png
│ │ └── looks_right_with_existing_email@2x.png
│ ├── RegistrationMobileViewControllerTests
│ │ ├── looks_right_by_default@2x.png
│ │ └── looks_right_with_existing_mobile@2x.png
│ ├── RegistrationPasswordViewControllerTests
│ │ ├── looks_right_by_default@2x.png
│ │ ├── looks_right_with_a_valid_password@2x.png
│ │ ├── looks_right_with_an_existing_email@2x.png
│ │ └── looks_right_with_an_invalid_password@2x.png
│ ├── RegistrationPostalZipViewControllerTests
│ │ ├── looks_right_by_default@2x.png
│ │ └── looks_right_with_existing_postal_code@2x.png
│ ├── SaleArtworkDetailsViewControllerTests
│ │ ├── looks_ok_by_default@2x.png
│ │ ├── with_a_buyers_premium__a_sale_artwork_details_view_controller__looks_ok_by_default@2x.png
│ │ ├── with_an_artwork_not_for_sale__a_sale_artwork_details_view_controller__looks_ok_by_default@2x.png
│ │ ├── with_lot_numbers__a_sale_artwork_details_view_controller__looks_ok_by_default@2x.png
│ │ └── without_lot_numbers__a_sale_artwork_details_view_controller__looks_ok_by_default@2x.png
│ ├── SwitchViewSpec
│ │ └── default@2x.png
│ └── YourBiddingDetailsViewControllerTests
│ │ └── displays_bidder_number_and_PIN@2x.png
├── RegisterFlowViewTests.swift
├── SaleArtworkDetailsViewControllerTests.swift
├── SwitchViewSpec.swift
├── TestHelpers.swift
├── TextFieldTests.swift
├── UILabelExtensionsTests.swift
├── XAppTokenSpec.swift
└── artwork.jpg
├── LICENSE
├── Podfile
├── Podfile.lock
├── README.md
├── docs
├── CI.md
├── PaymentProcessing.md
├── artsy_dev.md
├── cc_testing.png
├── deployment.md
├── eidolon_preview.jpg
├── manual flows.md
└── overview.md
└── fastlane
├── .env
├── Appfile
├── Fastfile
└── README.md
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | build:
4 | parallelism: 1
5 | environment:
6 | CIRCLE_ARTIFACTS: /tmp/circleci-artifacts
7 | CIRCLE_TEST_REPORTS: /tmp/circleci-test-results
8 | FASTLANE_SKIP_UPDATE_CHECK: true
9 | UPLOAD_IOS_SNAPSHOT_BUCKET_NAME: eidolon-ci
10 | # The following are required for CocoaPods-Keys to work.
11 | ArtsyAPIClientSecret: "-"
12 | ArtsyAPIClientKey: "-"
13 | HockeyProductionSecret: "-"
14 | HockeyBetaSecret: "-"
15 | SegmentWriteKey: "-"
16 | CardflightStagingAPIClientKey: "-"
17 | CardflightStagingMerchantAccountToken: "-"
18 | StripeStagingPublishableKey: "-"
19 | CardflightProductionAPIClientKey: "-"
20 | CardflightProductionMerchantAccountToken: "-"
21 | StripeProductionPublishableKey: "-"
22 | macos:
23 | xcode: 9.4.1
24 | shell: /bin/bash --login
25 |
26 | steps:
27 | - checkout
28 | - run: mkdir -p $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS derived_data
29 | - run: ruby -v
30 | - run: xcodebuild -version
31 |
32 | - run: xcrun simctl list
33 |
34 | # Try to restore installed Gems
35 | - restore_cache:
36 | keys:
37 | - v1-gems-{{ checksum "Gemfile.lock" }}
38 | - v1-gems-
39 | - run: bundle check || bundle install --path=vendor/bundle --jobs 4 --retry 3
40 | - save_cache:
41 | key: v1-gems-{{ checksum "Gemfile.lock" }}
42 | paths:
43 | - vendor/bundle
44 |
45 | # Try to restore CocoaPods dependencies
46 | - restore_cache:
47 | keys:
48 | - v2-pods-{{ checksum "Podfile.lock" }}
49 | - v2-pods-
50 |
51 | - run: bundle exec pod check || bundle exec pod install --verbose
52 |
53 | - save_cache:
54 | key: v2-pods-{{ checksum "Podfile.lock" }}
55 | paths:
56 | - Pods
57 |
58 | # Restore the derived data cache (to speed up CI compile times), run the tests, and store the cache
59 | - restore_cache:
60 | keys:
61 | - v1-derived-{{ .Branch }}
62 | - v1-derived-
63 | - run:
64 | name: Run tests
65 | command: set -o pipefail && xcodebuild -destination "name=iPad Air 2,OS=11.2" -scheme "Kiosk" -workspace "Kiosk.xcworkspace" -derivedDataPath derived_data build test | xcpretty --color
66 | - save_cache:
67 | key: v1-derived-{{ .Branch }}
68 | paths:
69 | - derived_data
70 |
71 | # Teardown
72 | # Save the Xcode activity log
73 | - run: find $HOME/Library/Developer/Xcode/DerivedData -name '*.xcactivitylog' -exec cp {} $CIRCLE_ARTIFACTS/xcactivitylog \; || true
74 | # Save test results
75 | - store_test_results:
76 | path: $CIRCLE_TEST_REPORTS
77 | # Save artifacts
78 | - store_artifacts:
79 | path: $ CIRCLE_ARTIFACTS
80 | - store_artifacts:
81 | path: $CIRCLE_TEST_REPORTS
82 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | CHANGELOG.yml merge=union
2 | *.pbxproj merge=union
3 |
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | .DS_Store
3 | build/
4 | *.pbxuser
5 | !default.pbxuser
6 | *.mode1v3
7 | !default.mode1v3
8 | *.mode2v3
9 | !default.mode2v3
10 | *.perspectivev3
11 | !default.perspectivev3
12 | *.xcworkspace
13 | !default.xcworkspace
14 | xcuserdata
15 | .xcuserstate
16 | profile
17 | *.moved-aside
18 | DerivedData
19 | .idea/
20 | Pods
21 | .pt
22 | .build
23 | tmtags
24 | tmtagsHistory
25 | config/releasenotes.txt
26 | .bundle
27 | *.xcuserstate
28 | Kiosk.xcodeproj/xcshareddata/xcschemes/Artsy.xcscheme
29 | Kiosk.xcworkspace/xcuserdata/*
30 | Podfile.local
31 | *.ipa
32 | fastlane/report.xml
33 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.4
2 |
--------------------------------------------------------------------------------
/.swift-version:
--------------------------------------------------------------------------------
1 | 3.0
2 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'cocoapods'
4 | # So we know if we need to run `pod install`
5 | gem 'cocoapods-check'
6 | gem 'cocoapods-keys'
7 |
8 | gem 'sbconstants'
9 | gem 'second_curtain'
10 | gem 'fastlane'
11 |
--------------------------------------------------------------------------------
/Kiosk.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Kiosk.xcodeproj/project.xcworkspace/xcshareddata/Kiosk.xccheckout:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDESourceControlProjectFavoriteDictionaryKey
6 |
7 | IDESourceControlProjectIdentifier
8 | E486DE6A-4C36-46CD-9CD3-03C3ED358081
9 | IDESourceControlProjectName
10 | Kiosk
11 | IDESourceControlProjectOriginsDictionary
12 |
13 | 362140352D1F3E008D207F94C7725A404E39F65C
14 | https://github.com/artsy/eidolon.git
15 |
16 | IDESourceControlProjectPath
17 | Kiosk.xcodeproj
18 | IDESourceControlProjectRelativeInstallPathDictionary
19 |
20 | 362140352D1F3E008D207F94C7725A404E39F65C
21 | ../..
22 |
23 | IDESourceControlProjectURL
24 | https://github.com/artsy/eidolon.git
25 | IDESourceControlProjectVersion
26 | 111
27 | IDESourceControlProjectWCCIdentifier
28 | 362140352D1F3E008D207F94C7725A404E39F65C
29 | IDESourceControlProjectWCConfigurations
30 |
31 |
32 | IDESourceControlRepositoryExtensionIdentifierKey
33 | public.vcs.git
34 | IDESourceControlWCCIdentifierKey
35 | 362140352D1F3E008D207F94C7725A404E39F65C
36 | IDESourceControlWCCName
37 | eidolon
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Kiosk.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Kiosk/Admin/AdminCardTestingViewController.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import RxSwift
3 | import Keys
4 |
5 | class AdminCardTestingViewController: UIViewController {
6 |
7 | lazy var keys = EidolonKeys()
8 | var cardHandler: CardHandler!
9 |
10 | @IBOutlet weak var logTextView: UITextView!
11 |
12 | override func viewDidLoad() {
13 | super.viewDidLoad()
14 |
15 |
16 | self.logTextView.text = ""
17 |
18 | if AppSetup.sharedState.useStaging {
19 | cardHandler = CardHandler(apiKey: self.keys.cardflightStagingAPIClientKey, accountToken: self.keys.cardflightStagingMerchantAccountToken)
20 | } else {
21 | cardHandler = CardHandler(apiKey: self.keys.cardflightProductionAPIClientKey, accountToken: self.keys.cardflightProductionMerchantAccountToken)
22 | }
23 |
24 | cardHandler.cardStatus
25 | .subscribe { (event) in
26 | switch event {
27 | case .next(let message):
28 | self.log("\(message)")
29 | case .error(let error):
30 | self.log("\n====Error====\n\(error)\nThe card reader may have become disconnected.\n\n")
31 | if self.cardHandler.card != nil {
32 | self.log("==\n\(self.cardHandler.card!)\n\n")
33 | }
34 | case .completed:
35 | guard let card = self.cardHandler.card else {
36 | // Restarts the card reader
37 | self.cardHandler.startSearching()
38 | return
39 | }
40 |
41 | let cardDetails = "Card: \(card.cardInfo.cardholderName ?? "") - \(card.cardInfo.lastFour ?? "") \n \(card.token)"
42 | self.log(cardDetails)
43 | }
44 | }
45 | .disposed(by: rx.disposeBag)
46 | }
47 |
48 | override func viewWillDisappear(_ animated: Bool) {
49 | super.viewWillDisappear(animated)
50 | cardHandler.end()
51 | }
52 |
53 | override func viewDidAppear(_ animated: Bool) {
54 | super.viewDidAppear(animated)
55 | cardHandler.startSearching()
56 | }
57 |
58 | func log(_ string: String) {
59 | self.logTextView.text = "\(self.logTextView.text ?? "")\n\(string)"
60 |
61 | }
62 |
63 | @IBAction func backTapped(_ sender: AnyObject) {
64 | _ = navigationController?.popViewController(animated: true)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Kiosk/Admin/AdminLogViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class AdminLogViewController: UIViewController {
4 |
5 | override func viewDidLoad() {
6 | super.viewDidLoad()
7 | textView.text = try? NSString(contentsOf: logPath(), encoding: String.Encoding.ascii.rawValue) as String
8 | }
9 |
10 | @IBOutlet weak var textView: UITextView!
11 | @IBAction func backButtonTapped(_ sender: AnyObject) {
12 | _ = self.navigationController?.popViewController(animated: true)
13 | }
14 |
15 | func logPath() -> URL {
16 | let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last!
17 | return docs.appendingPathComponent("logger.txt")
18 | }
19 |
20 | @IBAction func scrollTapped(_ sender: AnyObject) {
21 | textView.scrollRangeToVisible(NSMakeRange(textView.text.count - 1, 1))
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Kiosk/Admin/AuctionWebViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class AuctionWebViewController: WebViewController {
4 |
5 | override func viewDidLoad() {
6 | super.viewDidLoad()
7 |
8 | let flexibleSpace = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.flexibleSpace, target: nil, action: nil)
9 |
10 | let exitImage = UIImage(named: "toolbar_close")
11 | let backwardBarItem = UIBarButtonItem(image: exitImage, style: .plain, target: self, action: #selector(exit));
12 | let allItems = self.toolbarItems! + [flexibleSpace, backwardBarItem]
13 | toolbarItems = allItems
14 | }
15 |
16 | @objc func exit() {
17 | let passwordVC = PasswordAlertViewController.alertView { [weak self] in
18 | _ = self?.navigationController?.popViewController(animated: true)
19 | return
20 | }
21 | self.present(passwordVC, animated: true) {}
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Kiosk/Admin/ChooseAuctionViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import ORStackView
3 | import FLKAutoLayout
4 | import Artsy_UIFonts
5 | import Artsy_UIButtons
6 |
7 | class ChooseAuctionViewController: UIViewController {
8 |
9 | var auctions: [Sale]!
10 | let provider = appDelegate().provider
11 |
12 | override func viewDidLoad() {
13 | super.viewDidLoad()
14 | stackScrollView.backgroundColor = .white
15 | stackScrollView.updateConstraints()
16 |
17 | let endpoint: ArtsyAPI = ArtsyAPI.activeAuctions
18 |
19 | provider.request(endpoint)
20 | .filterSuccessfulStatusCodes()
21 | .mapJSON()
22 | .mapTo(arrayOf: Sale.self)
23 | .subscribe(onNext: { activeSales in
24 | self.auctions = activeSales
25 |
26 | for i in 0 ..< self.auctions.count {
27 | let sale = self.auctions[i]
28 | let title = " \(sale.name) - #\(sale.auctionState) - \(sale.artworkCount)"
29 |
30 | let button = ARFlatButton()
31 | button.setTitle(title, for: .normal)
32 | button.setTitleColor(.black, for: .normal)
33 | button.tag = i
34 | button.rx.tap.subscribe(onNext: { (_) in
35 | let defaults = UserDefaults.standard
36 | defaults.set(sale.id, forKey: "KioskAuctionID")
37 | defaults.synchronize()
38 | exit(1)
39 | })
40 | .disposed(by: self.rx.disposeBag)
41 |
42 | self.stackScrollView.stackView.addSubview(button, withTopMargin: "12", sideMargin: "0")
43 | button.constrainHeight("50")
44 | }
45 | })
46 | .disposed(by: rx.disposeBag)
47 |
48 | }
49 |
50 | @IBOutlet weak var stackScrollView: ORStackScrollView!
51 | @IBAction func backButtonTapped(_ sender: AnyObject) {
52 | _ = self.navigationController?.popViewController(animated: true)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Kiosk/Admin/PasswordAlertViewController.swift:
--------------------------------------------------------------------------------
1 |
2 | import UIKit
3 |
4 | class PasswordAlertViewController: UIAlertController {
5 |
6 | class func alertView(completion: @escaping () -> ()) -> PasswordAlertViewController {
7 | let alertController = PasswordAlertViewController(title: "Exit Kiosk", message: nil, preferredStyle: .alert)
8 | let exitAction = UIAlertAction(title: "Exit", style: .default) { (_) in
9 | completion()
10 | return
11 | }
12 |
13 | if detectDevelopmentEnvironment() {
14 | exitAction.isEnabled = true
15 | } else {
16 | exitAction.isEnabled = false
17 | }
18 |
19 | let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { (_) in }
20 |
21 | alertController.addTextField { (textField) in
22 | textField.placeholder = "Exit Password"
23 |
24 | NotificationCenter.default.addObserver(forName: NSNotification.Name.UITextFieldTextDidChange, object: textField, queue: OperationQueue.main) { (notification) in
25 | // compiler crashes when using weak
26 | exitAction.isEnabled = textField.text == "Genome401"
27 | }
28 | }
29 |
30 | alertController.addAction(exitAction)
31 | alertController.addAction(cancelAction)
32 | return alertController
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Kiosk/App/APIPingManager.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Moya
3 | import RxSwift
4 |
5 | class APIPingManager {
6 |
7 | let syncInterval: TimeInterval = 2
8 | var letOnline: Observable!
9 | var provider: Networking
10 |
11 | init(provider: Networking) {
12 | self.provider = provider
13 |
14 | letOnline = Observable.interval(syncInterval, scheduler: MainScheduler.instance)
15 | .flatMap { [weak self] _ in
16 | return self?.ping() ?? .empty()
17 | }
18 | .retry() // Retry because ping may fail when disconnected and error.
19 | .startWith(true)
20 | }
21 |
22 | fileprivate func ping() -> Observable {
23 | return provider.request(ArtsyAPI.ping).map(responseIsOK)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Kiosk/App/AppSetup.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class AppSetup {
4 |
5 | var auctionID = "los-angeles-modern-auctions-march-2015"
6 | lazy var useStaging = true
7 | lazy var showDebugButtons = false
8 | lazy var disableCardReader = false
9 | var isTesting = false
10 |
11 | class var sharedState : AppSetup {
12 | struct Static {
13 | static let instance = AppSetup()
14 | }
15 | return Static.instance
16 | }
17 |
18 | init() {
19 | let defaults = UserDefaults.standard
20 | if let auction = defaults.string(forKey: "KioskAuctionID") {
21 | auctionID = auction
22 | }
23 |
24 | useStaging = defaults.bool(forKey: "KioskUseStaging")
25 | showDebugButtons = defaults.bool(forKey: "KioskShowDebugButtons")
26 | disableCardReader = defaults.bool(forKey: "KioskDisableCardReader")
27 |
28 | if let _ = NSClassFromString("XCTest") { isTesting = true }
29 | }
30 |
31 | var needsZipCode: Bool {
32 | // If we're swiping with the card reader, we don't need to collect a zip code.
33 | return false
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Kiosk/App/Constants.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct AnimationDuration {
4 | static let Normal: TimeInterval = 0.30
5 | static let Short: TimeInterval = 0.15
6 | }
7 |
8 | let SyncInterval: TimeInterval = 60
9 | let ButtonHeight: CGFloat = 50
10 |
--------------------------------------------------------------------------------
/Kiosk/App/CreditCardValidation.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | BOOL validateCreditCardNumber(NSString *cardNumber);
5 |
6 |
--------------------------------------------------------------------------------
/Kiosk/App/CreditCardValidation.m:
--------------------------------------------------------------------------------
1 | #import "CreditCardValidation.h"
2 |
3 | BOOL validateCreditCardNumber(NSString *cardNumber) {
4 | STPCardValidationState validationState =
5 | [STPCardValidator validationStateForNumber:cardNumber validatingCardBrand:NO];
6 |
7 | switch (validationState) {
8 | case STPCardValidationStateValid:
9 | return YES;
10 | break;
11 | default:
12 | return NO;
13 | break;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Kiosk/App/KioskDateFormatter.h:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | @interface KioskDateFormatter :NSObject
4 |
5 | + (NSDate * __nullable)fromString:(NSString * _Nonnull)string;
6 |
7 | @end
8 |
--------------------------------------------------------------------------------
/Kiosk/App/KioskDateFormatter.m:
--------------------------------------------------------------------------------
1 | #import "KioskDateFormatter.h"
2 | @import ISO8601DateFormatter;
3 |
4 | @implementation KioskDateFormatter
5 |
6 | + (NSDate *)fromString:(NSString *)string {
7 | ISO8601DateFormatter *formatter = [[ISO8601DateFormatter alloc] init];
8 | return [formatter dateFromString:string];
9 | }
10 |
11 | @end
12 |
--------------------------------------------------------------------------------
/Kiosk/App/Logger.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | class Logger {
4 | let destination: URL
5 | lazy fileprivate var dateFormatter: DateFormatter = {
6 | let formatter = DateFormatter()
7 | formatter.locale = Locale.current
8 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
9 |
10 | return formatter
11 | }()
12 | lazy fileprivate var fileHandle: FileHandle? = {
13 | let path = self.destination.path
14 | FileManager.default.createFile(atPath: path, contents: nil, attributes: nil)
15 |
16 | do {
17 | let fileHandle = try FileHandle(forWritingTo: self.destination)
18 | print("Successfully logging to: \(path)")
19 | return fileHandle
20 | } catch let error as NSError {
21 | print("Serious error in logging: could not open path to log file. \(error).")
22 | }
23 |
24 | return nil
25 | }()
26 |
27 | init(destination: URL) {
28 | self.destination = destination
29 | }
30 |
31 | deinit {
32 | fileHandle?.closeFile()
33 | }
34 |
35 | func log(_ message: String, function: String = #function, file: String = #file, line: Int = #line) {
36 | let logMessage = stringRepresentation(message, function: function, file: file, line: line)
37 |
38 | printToConsole(logMessage)
39 | printToDestination(logMessage)
40 | }
41 | }
42 |
43 | private extension Logger {
44 | func stringRepresentation(_ message: String, function: String, file: String, line: Int) -> String {
45 | let dateString = dateFormatter.string(from: Date())
46 |
47 | let file = URL(fileURLWithPath: file).lastPathComponent
48 | return "\(dateString) [\(file):\(line)] \(function): \(message)\n"
49 | }
50 |
51 | func printToConsole(_ logMessage: String) {
52 | print(logMessage)
53 | }
54 |
55 | func printToDestination(_ logMessage: String) {
56 | if let data = logMessage.data(using: String.Encoding.utf8) {
57 | fileHandle?.write(data)
58 | } else {
59 | print("Serious error in logging: could not encode logged string into data.")
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Kiosk/App/MarkdownParser.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XNGMarkdownParser
3 | import Artsy_UIFonts
4 |
5 | class MarkdownParser: XNGMarkdownParser {
6 |
7 | override init() {
8 | super.init()
9 |
10 | paragraphFont = UIFont.serifFont(withSize: 16)
11 | linkFontName = UIFont.serifItalicFont(withSize: 16).fontName
12 | boldFontName = UIFont.serifBoldFont(withSize: 16).fontName
13 | italicFontName = UIFont.serifItalicFont(withSize: 16).fontName
14 | shouldParseLinks = false
15 |
16 | let paragraphStyle = NSMutableParagraphStyle()
17 | paragraphStyle.minimumLineHeight = 16
18 |
19 | topAttributes = [
20 | NSAttributedStringKey.paragraphStyle: paragraphStyle,
21 | NSAttributedStringKey.foregroundColor: UIColor.black
22 | ]
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Kiosk/App/Models/Artist.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftyJSON
3 |
4 | @objcMembers final class Artist: NSObject, JSONAbleType {
5 |
6 | let id: String
7 | dynamic var name: String
8 | let sortableID: String?
9 |
10 | var blurb: String?
11 |
12 | init(id: String, name: String, sortableID: String?) {
13 | self.id = id
14 | self.name = name
15 | self.sortableID = sortableID
16 | }
17 |
18 | static func fromJSON(_ json:[String: Any]) -> Artist {
19 | let json = JSON(json)
20 |
21 | let id = json["id"].stringValue
22 | let name = json["name"].stringValue
23 | let sortableID = json["sortable_id"].string
24 | return Artist(id: id, name:name, sortableID:sortableID)
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Kiosk/App/Models/Bid.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftyJSON
3 |
4 | final class Bid: NSObject, JSONAbleType {
5 | let id: String
6 | let amountCents: Currency
7 |
8 | init(id: String, amountCents: Currency) {
9 | self.id = id
10 | self.amountCents = amountCents
11 | }
12 |
13 | static func fromJSON(_ json:[String: Any]) -> Bid {
14 | let json = JSON(json)
15 |
16 | let id = json["id"].stringValue
17 | let amount = json["amount_cents"].uInt64
18 | return Bid(id: id, amountCents: amount!)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Kiosk/App/Models/Bidder.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftyJSON
3 |
4 | final class Bidder: NSObject, JSONAbleType {
5 | let id: String
6 | let saleID: String
7 | let createdByAdmin: Bool
8 | var pin: String?
9 |
10 | init(id: String, saleID: String, createdByAdmin: Bool, pin: String?) {
11 | self.id = id
12 | self.saleID = saleID
13 | self.createdByAdmin = createdByAdmin
14 | self.pin = pin
15 | }
16 |
17 | static func fromJSON(_ json:[String: Any]) -> Bidder {
18 | let json = JSON(json)
19 |
20 | let id = json["id"].stringValue
21 | let saleID = json["sale"]["id"].stringValue
22 | let createdByAdmin = json["created_by_admin"].bool ?? false
23 | let pin = json["pin"].stringValue
24 | return Bidder(id: id, saleID: saleID, createdByAdmin: createdByAdmin, pin: pin)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Kiosk/App/Models/BidderPosition.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftyJSON
3 |
4 | final class BidderPosition: NSObject, JSONAbleType {
5 | let id: String
6 | let highestBid: Bid?
7 | let maxBidAmountCents: Currency
8 | let processedAt: Date?
9 |
10 | init(id: String, highestBid: Bid?, maxBidAmountCents: Currency, processedAt: Date?) {
11 | self.id = id
12 | self.highestBid = highestBid
13 | self.maxBidAmountCents = maxBidAmountCents
14 | self.processedAt = processedAt
15 | }
16 |
17 | static func fromJSON(_ source: [String: Any]) -> BidderPosition {
18 | let json = JSON(source)
19 |
20 | let id = json["id"].stringValue
21 | let maxBidAmount = json["max_bid_amount_cents"].uInt64Value
22 | let processedAt = KioskDateFormatter.fromString(json["processed_at"].stringValue)
23 |
24 | var bid: Bid?
25 | if let bidDictionary = json["highest_bid"].object as? [String: AnyObject] {
26 | bid = Bid.fromJSON(bidDictionary)
27 | }
28 |
29 | return BidderPosition(id: id, highestBid: bid, maxBidAmountCents: maxBidAmount, processedAt: processedAt)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Kiosk/App/Models/BuyersPremium.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftyJSON
3 |
4 | final class BuyersPremium: NSObject, JSONAbleType {
5 | let id: String
6 | let name: String
7 |
8 | init(id: String, name: String) {
9 | self.id = id
10 | self.name = name
11 | }
12 |
13 | static func fromJSON(_ json: [String: Any]) -> BuyersPremium {
14 | let json = JSON(json)
15 | let id = json["id"].stringValue
16 | let name = json["name"].stringValue
17 |
18 | return BuyersPremium(id: id, name: name)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Kiosk/App/Models/Card.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftyJSON
3 |
4 | final class Card: NSObject, JSONAbleType {
5 | let id: String
6 | let name: String
7 | let lastDigits: String
8 | let expirationMonth: String
9 | let expirationYear: String
10 |
11 | init(id: String, name: String, lastDigits: String, expirationMonth: String, expirationYear: String) {
12 |
13 | self.id = id
14 | self.name = name
15 | self.lastDigits = lastDigits
16 | self.expirationMonth = expirationMonth
17 | self.expirationYear = expirationYear
18 | }
19 |
20 | static func fromJSON(_ json:[String: Any]) -> Card {
21 | let json = JSON(json)
22 |
23 | let id = json["id"].stringValue
24 | let name = json["name"].stringValue
25 | let lastDigits = json["last_digits"].stringValue
26 | let expirationMonth = json["expiration_month"].stringValue
27 | let expirationYear = json["expiration_year"].stringValue
28 |
29 | return Card(id: id, name: name, lastDigits: lastDigits, expirationMonth: expirationMonth, expirationYear: expirationYear)
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/Kiosk/App/Models/CreditCard.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // This protocol represents a credit card from Stripe. But they recently changed their API so we can't mock things
4 | // using it anymore, so we need to wrap their API in a protocol (so that we can mock it in tests).
5 | protocol CreditCard {
6 | var name: String? { get }
7 | var brandName: String? { get }
8 | var last4: String { get }
9 | }
10 |
11 | import Stripe
12 | extension STPCard: CreditCard {
13 | var brandName: String? {
14 | return self.brand.name
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Kiosk/App/Models/GenericError.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftyJSON
3 |
4 | final class GenericError: NSObject, JSONAbleType {
5 | let detail: [String:AnyObject]
6 | let message: String
7 | let type: String
8 |
9 | init(type: String, message: String, detail: [String:AnyObject]) {
10 | self.detail = detail
11 | self.message = message
12 | self.type = type
13 | }
14 |
15 | static func fromJSON(_ json:[String: Any]) -> GenericError {
16 | let json = JSON(json)
17 |
18 | let type = json["type"].stringValue
19 | let message = json["message"].stringValue
20 | var detailDictionary = json["detail"].object as? [String: AnyObject]
21 |
22 | detailDictionary = detailDictionary ?? [:]
23 | return GenericError(type: type, message: message, detail: detailDictionary!)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Kiosk/App/Models/JSONAble.swift:
--------------------------------------------------------------------------------
1 | protocol JSONAbleType {
2 | static func fromJSON(_: [String: Any]) -> Self
3 | }
4 |
--------------------------------------------------------------------------------
/Kiosk/App/Models/Location.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftyJSON
3 |
4 | final class Location: NSObject, JSONAbleType {
5 | let address: String
6 | let address2: String
7 | let city: String
8 | let state: String
9 | let stateCode: String
10 | var postalCode: String
11 |
12 | init(address: String, address2: String, city: String, state: String, stateCode: String, postalCode: String) {
13 | self.address = address
14 | self.address2 = address2
15 | self.city = city
16 | self.state = state
17 | self.stateCode = stateCode
18 | self.postalCode = postalCode
19 | }
20 |
21 | static func fromJSON(_ json: [String: Any]) -> Location {
22 | let json = JSON(json)
23 |
24 | let address = json["address"].stringValue
25 | let address2 = json["address_2"].stringValue
26 | let city = json["city"].stringValue
27 | let state = json["state"].stringValue
28 | let stateCode = json["state_code"].stringValue
29 | let postalCode = json["postal_code"].stringValue
30 |
31 | return Location(address: address, address2: address2, city: city, state: state, stateCode: stateCode, postalCode: postalCode)
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/Kiosk/App/Models/Sale.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftyJSON
3 |
4 | @objcMembers final class Sale: NSObject, JSONAbleType {
5 | dynamic let id: String
6 | dynamic let isAuction: Bool
7 | dynamic let startDate: Date
8 | dynamic let endDate: Date?
9 | dynamic let name: String
10 | dynamic var artworkCount: Int
11 | dynamic let auctionState: String
12 |
13 | dynamic var buyersPremium: BuyersPremium?
14 |
15 | init(id: String, name: String, isAuction: Bool, startDate: Date, endDate: Date?, artworkCount: Int, state: String) {
16 | self.id = id
17 | self.name = name
18 | self.isAuction = isAuction
19 | self.startDate = startDate
20 | self.endDate = endDate
21 | self.artworkCount = artworkCount
22 | self.auctionState = state
23 | }
24 |
25 | static func fromJSON(_ json:[String: Any]) -> Sale {
26 | let json = JSON(json)
27 |
28 | let id = json["id"].stringValue
29 | let isAuction = json["is_auction"].boolValue
30 | let startDate = KioskDateFormatter.fromString(json["start_at"].stringValue)!
31 | let endDate = KioskDateFormatter.fromString(json["end_at"].stringValue)
32 | let name = json["name"].stringValue
33 | let artworkCount = json["eligible_sale_artworks_count"].intValue
34 | let state = json["auction_state"].stringValue
35 |
36 | let sale = Sale(id: id, name:name, isAuction: isAuction, startDate: startDate, endDate: endDate, artworkCount: artworkCount, state: state)
37 |
38 | if let buyersPremiumDict = json["buyers_premium"].object as? [String: AnyObject] {
39 | sale.buyersPremium = BuyersPremium.fromJSON(buyersPremiumDict)
40 | }
41 |
42 | return sale
43 | }
44 |
45 | func isActive(_ systemTime:SystemTime) -> Bool {
46 | let now = systemTime.date()
47 | guard let endDate = endDate else {
48 | // Live sales don't have end dates, and the kiosk isn't compatible with live sales.
49 | // So we'll say any live sale is "not active"
50 | return false
51 | }
52 | return (now as NSDate).earlierDate(startDate) == startDate && (now as NSDate).laterDate(endDate) == endDate
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Kiosk/App/Models/SystemTime.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import RxSwift
3 |
4 | class SystemTime {
5 | var systemTimeInterval: TimeInterval? = nil
6 |
7 | init () {}
8 |
9 | func sync(_ provider: Networking) -> Observable {
10 | let endpoint: ArtsyAPI = ArtsyAPI.systemTime
11 |
12 | return provider.request(endpoint)
13 | .filterSuccessfulStatusCodes()
14 | .mapJSON()
15 | .do(onNext: { [weak self] response in
16 | guard let dictionary = response as? NSDictionary else { return }
17 |
18 | let timestamp: String = (dictionary["iso8601"] as? String) ?? ""
19 | if let artsyDate = KioskDateFormatter.fromString(timestamp) {
20 | self?.systemTimeInterval = Date().timeIntervalSince(artsyDate)
21 | }
22 |
23 | })
24 | .logError()
25 | .map(void)
26 | }
27 |
28 | func inSync() -> Bool {
29 | return systemTimeInterval != nil
30 | }
31 |
32 | func date() -> Date {
33 | let now = Date()
34 | if let systemTimeInterval = systemTimeInterval {
35 | return now.addingTimeInterval(-systemTimeInterval)
36 | } else {
37 | return now
38 | }
39 | }
40 |
41 | func reset() {
42 | systemTimeInterval = nil
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Kiosk/App/Models/User.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftyJSON
3 |
4 | @objcMembers final class User: NSObject, JSONAbleType {
5 |
6 | dynamic let id: String
7 | dynamic let email: String
8 | dynamic let name: String
9 | dynamic let paddleNumber: String
10 | dynamic let phoneNumber: String
11 | dynamic var bidder: Bidder?
12 | dynamic var location: Location?
13 |
14 | init(id: String, email: String, name: String, paddleNumber: String, phoneNumber: String, location: Location?) {
15 | self.id = id
16 | self.name = name
17 | self.paddleNumber = paddleNumber
18 | self.email = email
19 | self.phoneNumber = phoneNumber
20 | self.location = location
21 | }
22 |
23 | static func fromJSON(_ json: [String: Any]) -> User {
24 | let json = JSON(json)
25 |
26 | let id = json["id"].stringValue
27 | let name = json["name"].stringValue
28 | let email = json["email"].stringValue
29 | let paddleNumber = json["paddle_number"].stringValue
30 | let phoneNumber = json["phone"].stringValue
31 |
32 | var location:Location?
33 | if let bidDictionary = json["location"].object as? [String: AnyObject] {
34 | location = Location.fromJSON(bidDictionary)
35 | }
36 |
37 |
38 | return User(id: id, email: email, name: name, paddleNumber: paddleNumber, phoneNumber: phoneNumber, location:location)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Kiosk/App/NSErrorExtensions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Moya
3 |
4 | extension NSError {
5 |
6 | func artsyServerError() -> NSString {
7 | if let errorJSON = userInfo["data"] as? [String: AnyObject] {
8 | let error = GenericError.fromJSON(errorJSON)
9 | return "\(error.message) - \(error.detail) + \(error.detail)" as NSString
10 | } else if let response = userInfo["data"] as? Response {
11 | let stringData = NSString(data: response.data, encoding: String.Encoding.utf8.rawValue)
12 | return "Status Code: \(response.statusCode), Data Length: \(response.data.count), String Data: \(String(describing: stringData))" as NSString
13 | }
14 |
15 | return "\(userInfo)" as NSString
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Kiosk/App/Networking/APIKeys.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Keys
3 |
4 | private let minimumKeyLength = 2
5 |
6 | // Mark: - API Keys
7 |
8 | struct APIKeys {
9 | let key: String
10 | let secret: String
11 |
12 | // MARK: Shared Keys
13 |
14 | fileprivate struct SharedKeys {
15 | static var instance = APIKeys()
16 | }
17 |
18 | static var sharedKeys: APIKeys {
19 | get {
20 | return SharedKeys.instance
21 | }
22 |
23 | set (newSharedKeys) {
24 | SharedKeys.instance = newSharedKeys
25 | }
26 | }
27 |
28 | // MARK: Methods
29 |
30 | var stubResponses: Bool {
31 | return key.count < minimumKeyLength || secret.count < minimumKeyLength
32 | }
33 |
34 | // MARK: Initializers
35 |
36 | init(key: String, secret: String) {
37 | self.key = key
38 | self.secret = secret
39 | }
40 |
41 | init(keys: EidolonKeys) {
42 | self.init(key: keys.artsyAPIClientKey , secret: keys.artsyAPIClientSecret )
43 | }
44 |
45 | init() {
46 | let keys = EidolonKeys()
47 | self.init(keys: keys)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Kiosk/App/Networking/NetworkLogger.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Moya
3 | import Result
4 |
5 | /// Logs network activity (outgoing requests and incoming responses).
6 | class NetworkLogger: PluginType {
7 |
8 | typealias Comparison = (TargetType) -> Bool
9 |
10 | let whitelist: Comparison
11 | let blacklist: Comparison
12 |
13 | init(whitelist: @escaping Comparison = { _ -> Bool in return true }, blacklist: @escaping Comparison = { _ -> Bool in return true }) {
14 | self.whitelist = whitelist
15 | self.blacklist = blacklist
16 | }
17 |
18 | func willSendRequest(_ request: RequestType, target: TargetType) {
19 | // If the target is in the blacklist, don't log it.
20 | guard blacklist(target) == false else { return }
21 | logger.log("Sending request: \(request.request?.url?.absoluteString ?? String())")
22 | }
23 |
24 | func didReceiveResponse(_ result: Result, target: TargetType) {
25 | // If the target is in the blacklist, don't log it.
26 | guard blacklist(target) == false else { return }
27 |
28 | switch result {
29 | case .success(let response):
30 | if 200..<400 ~= (response.statusCode ) && whitelist(target) == false {
31 | // If the status code is OK, and if it's not in our whitelist, then don't worry about logging its response body.
32 | logger.log("Received response(\(response.statusCode )) from \(response.response?.url?.absoluteString ?? String()).")
33 | }
34 | case .failure(let error):
35 | // Otherwise, log everything.
36 | logger.log("Received networking error: \(error)")
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Kiosk/App/Networking/UserCredentials.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct UserCredentials {
4 | let user: User
5 | let accessToken: String
6 |
7 | init(user: User, accessToken: String){
8 | self.user = user
9 | self.accessToken = accessToken
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Kiosk/App/Networking/XAppToken.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | private extension Date {
4 | var isInPast: Bool {
5 | let now = Date()
6 | return self.compare(now) == ComparisonResult.orderedAscending
7 | }
8 | }
9 |
10 | struct XAppToken {
11 | enum DefaultsKeys: String {
12 | case TokenKey = "TokenKey"
13 | case TokenExpiry = "TokenExpiry"
14 | }
15 |
16 | // MARK: - Initializers
17 |
18 | let defaults: UserDefaults
19 |
20 | init(defaults: UserDefaults) {
21 | self.defaults = defaults
22 | }
23 |
24 | init() {
25 | self.defaults = UserDefaults.standard
26 | }
27 |
28 |
29 | // MARK: - Properties
30 |
31 | var token: String? {
32 | get {
33 | let key = defaults.string(forKey: DefaultsKeys.TokenKey.rawValue)
34 | return key
35 | }
36 | set(newToken) {
37 | defaults.set(newToken, forKey: DefaultsKeys.TokenKey.rawValue)
38 | }
39 | }
40 |
41 | var expiry: Date? {
42 | get {
43 | return defaults.object(forKey: DefaultsKeys.TokenExpiry.rawValue) as? Date
44 | }
45 | set(newExpiry) {
46 | defaults.set(newExpiry, forKey: DefaultsKeys.TokenExpiry.rawValue)
47 | }
48 | }
49 |
50 | var expired: Bool {
51 | if let expiry = expiry {
52 | return expiry.isInPast
53 | }
54 | return true
55 | }
56 |
57 | var isValid: Bool {
58 | if let token = token {
59 | return token.isNotEmpty && !expired
60 | }
61 |
62 | return false
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Kiosk/App/OfflineViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Artsy_UIFonts
3 |
4 | class OfflineViewController: UIViewController {
5 | @IBOutlet var offlineLabel: UILabel!
6 | @IBOutlet var subtitleLabel: UILabel!
7 |
8 | override func viewDidLoad() {
9 | super.viewDidLoad()
10 |
11 | offlineLabel.font = UIFont.serifFont(withSize: 49)
12 | subtitleLabel.font = UIFont.serifItalicFont(withSize: 32)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Kiosk/App/StubResponses.h:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | @interface StubResponses : NSObject
4 |
5 | + (BOOL)stubResponses;
6 |
7 | @end
8 |
--------------------------------------------------------------------------------
/Kiosk/App/StubResponses.m:
--------------------------------------------------------------------------------
1 | #import "StubResponses.h"
2 |
3 | #define STUB_RESPONSES YES
4 |
5 | // We're inferring if you have access to the private Artsy fonts, then you have access to our API.
6 | #if __has_include()
7 | #undef STUB_RESPONSES
8 | #define STUB_RESPONSES NO
9 | #endif
10 |
11 | @implementation StubResponses
12 |
13 | + (BOOL)stubResponses {
14 | return STUB_RESPONSES;
15 | }
16 |
17 | @end
18 |
--------------------------------------------------------------------------------
/Kiosk/App/SwiftExtensions.swift:
--------------------------------------------------------------------------------
1 | import RxOptional
2 |
3 | extension Optional {
4 | var hasValue: Bool {
5 | switch self {
6 | case .none:
7 | return false
8 | case .some(_):
9 | return true
10 | }
11 | }
12 | }
13 |
14 | extension String {
15 | func toUInt() -> UInt? {
16 | return UInt(self)
17 | }
18 |
19 | func toUInt(withDefault defaultValue: UInt) -> UInt {
20 | return UInt(self) ?? defaultValue
21 | }
22 | }
23 |
24 |
25 | // Extend the idea of occupiability to optionals. Specifically, optionals wrapping occupiable things.
26 | // We're relying on the RxOptional pod to provide the Occupiable protocol.
27 | extension Optional where Wrapped: Occupiable {
28 | var isNilOrEmpty: Bool {
29 | switch self {
30 | case .none:
31 | return true
32 | case .some(let value):
33 | return value.isEmpty
34 | }
35 | }
36 |
37 | var isNotNilNotEmpty: Bool {
38 | return !isNilOrEmpty
39 | }
40 | }
41 |
42 |
43 | extension NSNumber {
44 | var currencyValue: Currency {
45 | return uint64Value
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Kiosk/App/UIStoryboardSegueExtensions.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UIStoryboardSegue {
4 | }
5 |
6 | func ==(lhs: UIStoryboardSegue, rhs: SegueIdentifier) -> Bool {
7 | return lhs.identifier == rhs.rawValue
8 | }
9 |
--------------------------------------------------------------------------------
/Kiosk/App/UIViewControllerExtensions.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UIViewController {
4 |
5 | /// Short hand syntax for loading the view controller
6 |
7 | func loadViewProgrammatically(){
8 | self.beginAppearanceTransition(true, animated: false)
9 | self.endAppearanceTransition()
10 | }
11 |
12 | /// Short hand syntax for performing a segue with a known hardcoded identity
13 |
14 | func performSegue(_ identifier:SegueIdentifier) {
15 | self.performSegue(withIdentifier: identifier.rawValue, sender: self)
16 | }
17 |
18 | func fulfillmentNav() -> FulfillmentNavigationController {
19 | return (navigationController! as! FulfillmentNavigationController)
20 | }
21 |
22 | func fulfillmentContainer() -> FulfillmentContainerViewController? {
23 | return fulfillmentNav().parent as? FulfillmentContainerViewController
24 | }
25 |
26 | func findChildViewControllerOfType(_ klass: AnyClass) -> UIViewController? {
27 | for child in childViewControllers {
28 | if child.isKind(of: klass) {
29 | return child
30 | }
31 | }
32 | return nil
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Kiosk/App/UIViewSubclassesErrorExtensions.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension Button {
4 |
5 | func flashError(_ message:String) {
6 | let originalTitle = self.title(for: .normal)
7 |
8 | setTitleColor(.white, for: .disabled)
9 | setBackgroundColor(.artsyRedRegular(), for: .disabled, animated: true)
10 | setBorderColor(.artsyRedRegular(), for: .disabled, animated: true)
11 |
12 | setTitle(message.uppercased(), for: .disabled)
13 |
14 | delayToMainThread(2) {
15 | self.setTitleColor(.artsyGrayMedium(), for: .disabled)
16 | self.setBackgroundColor(.white, for: .disabled, animated: true)
17 | self.setTitle(originalTitle, for: .disabled)
18 | self.setBorderColor(.artsyGrayMedium(), for: .disabled, animated: true)
19 | }
20 | }
21 | }
22 |
23 | extension TextField {
24 |
25 | func flashForError() {
26 | self.setBorderColor(.artsyRedRegular())
27 | delayToMainThread(2) {
28 | self.setBorderColor(.artsyPurpleRegular())
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Kiosk/App/Views/SimulatorOnlyView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class DeveloperOnlyView: UIView {
4 |
5 | override func awakeFromNib() {
6 | // Show only if we're supposed to show AND we're on staging.
7 | self.isHidden = !(AppSetup.sharedState.showDebugButtons && AppSetup.sharedState.useStaging)
8 |
9 | if let _ = NSClassFromString("XCTest") {
10 | // We are running in a test.
11 | self.isHidden = true
12 | self.alpha = 0
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Kiosk/App/Views/Spinner.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class Spinner: UIView {
4 | var spinner:UIView!
5 | let rotationDuration = 0.9;
6 |
7 | func createSpinner() -> UIView {
8 | let view = UIView(frame: CGRect(x: 0, y: 0, width: 20, height: 5))
9 | view.backgroundColor = .black
10 | return view
11 | }
12 |
13 | override func awakeFromNib() {
14 | spinner = createSpinner()
15 | addSubview(spinner)
16 | backgroundColor = .clear
17 | animateN(Float.infinity)
18 | }
19 |
20 | override func layoutSubviews() {
21 | // .center uses frame
22 | spinner.center = CGPoint( x: bounds.width / 2, y: bounds.height / 2)
23 | }
24 |
25 | func animateN(_ times: Float) {
26 | let transformOffset = -1.01 * CGFloat.pi
27 | let transform = CATransform3DMakeRotation( CGFloat(transformOffset), 0, 0, 1);
28 | let rotationAnimation = CABasicAnimation(keyPath:"transform");
29 |
30 | rotationAnimation.toValue = NSValue(caTransform3D:transform)
31 | rotationAnimation.duration = rotationDuration;
32 | rotationAnimation.isCumulative = true;
33 | rotationAnimation.repeatCount = Float(times);
34 | layer.add(rotationAnimation, forKey:"spin");
35 | }
36 |
37 | func animate(_ animate: Bool) {
38 | let isAnimating = layer.animation(forKey: "spin") != nil
39 | if (isAnimating && !animate) {
40 | layer.removeAllAnimations()
41 |
42 | } else if (!isAnimating && animate) {
43 | self.animateN(Float.infinity)
44 | }
45 | }
46 |
47 | func stopAnimating() {
48 | layer.removeAllAnimations()
49 | animateN(1)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Kiosk/App/Views/Text Fields/CursorView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class CursorView: UIView {
4 |
5 | let cursorLayer: CALayer = CALayer()
6 |
7 | override init(frame: CGRect) {
8 | super.init(frame: frame)
9 | setup()
10 | }
11 |
12 | required init?(coder aDecoder: NSCoder) {
13 | super.init(coder: aDecoder)
14 | setup()
15 | }
16 |
17 | override func awakeFromNib() {
18 | setupCursorLayer()
19 | startAnimating()
20 | }
21 |
22 | func setup() {
23 | layer.addSublayer(cursorLayer)
24 | setupCursorLayer()
25 | }
26 |
27 | func setupCursorLayer() {
28 | cursorLayer.frame = CGRect(x: layer.frame.width/2 - 1, y: 0, width: 2, height: layer.frame.height)
29 | cursorLayer.backgroundColor = UIColor.black.cgColor
30 | cursorLayer.opacity = 0.0
31 | }
32 |
33 | func startAnimating() {
34 | animate(Float.infinity)
35 | }
36 |
37 | fileprivate func animate(_ times: Float) {
38 | let fade = CABasicAnimation()
39 | fade.duration = 0.5
40 | fade.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
41 | fade.repeatCount = times
42 | fade.autoreverses = true
43 | fade.fromValue = 0.0
44 | fade.toValue = 1.0
45 | cursorLayer.add(fade, forKey: "opacity")
46 | }
47 |
48 | func stopAnimating() {
49 | cursorLayer.removeAllAnimations()
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Kiosk/Auction Listings/WebViewController.swift:
--------------------------------------------------------------------------------
1 | import DZNWebViewController
2 |
3 | let modalHeight: CGFloat = 660
4 |
5 | class WebViewController: DZNWebViewController {
6 | var showToolbar = true
7 |
8 | convenience override init(url: URL) {
9 | self.init()
10 | self.url = url
11 | }
12 |
13 | override func viewDidLoad() {
14 | super.viewDidLoad()
15 |
16 | let webView = view as! UIWebView
17 | webView.scalesPageToFit = true
18 |
19 | self.navigationItem.rightBarButtonItem = nil
20 | }
21 |
22 | override func viewWillAppear(_ animated: Bool) {
23 | super.viewWillAppear(animated)
24 | navigationController?.setNavigationBarHidden(true, animated:false)
25 | navigationController?.setToolbarHidden(!showToolbar, animated:false)
26 | }
27 | }
28 |
29 | class ModalWebViewController: WebViewController {
30 | var closeButton: UIButton!
31 |
32 | override func viewDidLoad() {
33 | super.viewDidLoad()
34 |
35 | closeButton = UIButton()
36 | view.addSubview(closeButton)
37 | closeButton.titleLabel?.font = UIFont.sansSerifFont(withSize: 14)
38 | closeButton.setTitleColor(.artsyGrayMedium(), for:.normal)
39 | closeButton.setTitle("CLOSE", for:.normal)
40 | closeButton.constrainWidth("140", height: "72")
41 | closeButton.alignTop("0", leading:"0", bottom:nil, trailing:nil, to:view)
42 | closeButton.addTarget(self, action:#selector(closeTapped(_:)), for:.touchUpInside)
43 |
44 | var height = modalHeight
45 | if let nav = navigationController {
46 | if !nav.isNavigationBarHidden { height -= nav.navigationBar.frame.height }
47 | if !nav.isToolbarHidden { height -= nav.toolbar.frame.height }
48 | }
49 | preferredContentSize = CGSize(width: 815, height: height)
50 | }
51 |
52 |
53 | override func viewWillAppear(_ animated: Bool) {
54 | super.viewWillAppear(animated)
55 | navigationController?.view.superview?.layer.cornerRadius = 0;
56 | }
57 |
58 | @objc func closeTapped(_ sender: AnyObject) {
59 | presentingViewController?.dismiss(animated: true, completion:nil)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Kiosk/Bid Fulfillment/AdminCCBypassNetworkModel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import RxSwift
3 | import Moya
4 |
5 | enum BypassResult {
6 | case requireCC
7 | case skipCCRequirement
8 | }
9 |
10 | protocol AdminCCBypassNetworkModelType {
11 |
12 | func checkForAdminCCBypass(_ saleID: String, authorizedNetworking: AuthorizedNetworking) -> Observable
13 | }
14 |
15 | class AdminCCBypassNetworkModel: AdminCCBypassNetworkModelType {
16 |
17 | /// Returns an Observable of (Bool, AuthorizedNetworking)
18 | /// The Bool represents if the Credit Card requirement should be waived.
19 | /// THe AuthorizedNetworking is the same instance that's passed in, which is a convenience for chaining observables.
20 | func checkForAdminCCBypass(_ saleID: String, authorizedNetworking: AuthorizedNetworking) -> Observable {
21 |
22 | return authorizedNetworking
23 | .request(ArtsyAuthenticatedAPI.findMyBidderRegistration(auctionID: saleID))
24 | .filterSuccessfulStatusCodes()
25 | .mapJSON()
26 | .mapTo(arrayOf: Bidder.self)
27 | .map { bidders in
28 | return bidders.first
29 | }
30 | .map { bidder -> BypassResult in
31 | guard let bidder = bidder else { return .requireCC }
32 |
33 | switch bidder.createdByAdmin {
34 | case true: return .skipCCRequirement
35 | case false: return .requireCC
36 | }
37 | }
38 | .logError()
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Kiosk/Bid Fulfillment/ConfirmYourBidPasswordViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | // Unused ATM
4 |
5 | class ConfirmYourBidPasswordViewController: UIViewController {
6 |
7 | @IBOutlet var bidDetailsPreviewView: BidDetailsPreviewView!
8 |
9 | class func instantiateFromStoryboard(_ storyboard: UIStoryboard) -> ConfirmYourBidPasswordViewController {
10 | return storyboard.viewController(withID: .ConfirmYourBid) as! ConfirmYourBidPasswordViewController
11 | }
12 |
13 | override func viewDidLoad() {
14 | super.viewDidLoad()
15 |
16 | bidDetailsPreviewView.bidDetails = fulfillmentNav().bidDetails
17 | }
18 |
19 | @IBAction func dev_noPhoneNumberFoundTapped(_ sender: AnyObject) {
20 |
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/Kiosk/Bid Fulfillment/FulfillmentContainerViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class FulfillmentContainerViewController: UIViewController {
4 | var allowAnimations = true
5 |
6 | @IBOutlet var cancelButton: UIButton!
7 | @IBOutlet var contentView: UIView!
8 | @IBOutlet var backgroundView: UIView!
9 |
10 | override func viewDidLoad() {
11 | super.viewDidLoad()
12 | modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
13 |
14 | contentView.alpha = 0
15 | backgroundView.alpha = 0
16 | cancelButton.alpha = 0
17 | }
18 |
19 | // We force viewDidAppear to access the PlaceBidViewController
20 | // so this allow animations in the modal
21 |
22 | // This is mostly a placeholder for a more complex animation in the future
23 |
24 | func viewDidAppearAnimation(_ animated: Bool) {
25 | self.contentView.frame = self.contentView.frame.offsetBy(dx: 0, dy: 100)
26 | UIView.animateTwoStepIf(animated, duration: 0.3, {
27 | self.backgroundView.alpha = 1
28 |
29 | }, midway: {
30 | self.contentView.alpha = 1
31 | self.cancelButton.alpha = 1
32 | self.contentView.frame = self.contentView.frame.offsetBy(dx: 0, dy: -100)
33 | }) { (complete) in
34 |
35 | }
36 | }
37 |
38 | @IBAction func closeModalTapped(_ sender: AnyObject) {
39 | closeFulfillmentModal()
40 | }
41 |
42 | func closeFulfillmentModal(completion: (() -> ())? = nil) -> Void {
43 | UIView.animateIf(allowAnimations, duration: 0.4, {
44 | self.contentView.alpha = 0
45 | self.backgroundView.alpha = 0
46 | self.cancelButton.alpha = 0
47 |
48 | }) { (completed:Bool) in
49 | let presentingVC = self.presentingViewController!
50 | presentingVC.dismiss(animated: false, completion: nil)
51 | completion?()
52 | }
53 | }
54 |
55 | func internalNavigationController() -> FulfillmentNavigationController? {
56 |
57 | self.loadViewProgrammatically()
58 | return self.childViewControllers.first as? FulfillmentNavigationController
59 | }
60 |
61 | class func instantiateFromStoryboard(_ storyboard: UIStoryboard) -> FulfillmentContainerViewController {
62 | return storyboard.viewController(withID: .FulfillmentContainer) as! FulfillmentContainerViewController
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Kiosk/Bid Fulfillment/FulfillmentNavigationController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Moya
3 | import RxSwift
4 |
5 | // We abstract this out so that we don't have network models, etc, aware of the view controller.
6 | // This is a "source of truth" that should be referenced in lieu of many independent variables.
7 | protocol FulfillmentController: class {
8 | var bidDetails: BidDetails { get set }
9 | var auctionID: String! { get set }
10 | }
11 |
12 | class FulfillmentNavigationController: UINavigationController, FulfillmentController {
13 |
14 | // MARK: - FulfillmentController bits
15 |
16 | /// The the collection of details necessary to eventually create a bid
17 | lazy var bidDetails: BidDetails = {
18 | return BidDetails(saleArtwork:nil, paddleNumber: nil, bidderPIN: nil, bidAmountCents:nil, auctionID: self.auctionID)
19 | }()
20 | var auctionID: String!
21 | var user: User!
22 |
23 | var provider: Networking!
24 |
25 | // MARK: - Everything else
26 |
27 | override func viewDidLoad() {
28 | super.viewDidLoad()
29 | self.delegate = self
30 | }
31 |
32 | func reset() {
33 | let storage = HTTPCookieStorage.shared
34 | let cookies = storage.cookies
35 | cookies?.forEach { storage.deleteCookie($0) }
36 | }
37 |
38 | func updateUserCredentials(loggedInProvider: AuthorizedNetworking) -> Observable {
39 | let endpoint = ArtsyAuthenticatedAPI.me
40 | let request = loggedInProvider.request(endpoint).filterSuccessfulStatusCodes().mapJSON().mapTo(object: User.self)
41 |
42 | return request
43 | .do(onNext: { [weak self] fullUser in
44 | guard let me = self else { return }
45 |
46 | me.user = fullUser
47 |
48 | let newUser = me.bidDetails.newUser
49 |
50 | newUser.email.value = me.user.email
51 | newUser.phoneNumber.value = me.user.phoneNumber
52 | newUser.zipCode.value = me.user.location?.postalCode
53 | newUser.name.value = me.user.name
54 | })
55 | .logError(prefix: "error, the authentication for admin is likely wrong: ")
56 | .map(void)
57 | }
58 | }
59 |
60 | extension FulfillmentNavigationController: UINavigationControllerDelegate {
61 | func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
62 | guard let viewController = viewController as? PlaceBidViewController else { return }
63 |
64 | viewController.provider = provider
65 | }
66 | }
67 |
68 |
--------------------------------------------------------------------------------
/Kiosk/Bid Fulfillment/GenericFormValidationViewModel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import RxSwift
3 | import Action
4 |
5 | class GenericFormValidationViewModel {
6 | let command: CocoaAction
7 | let disposeBag = DisposeBag()
8 |
9 | init(isValid: Observable, manualInvocation: Observable, finishedSubject: PublishSubject) {
10 |
11 | command = CocoaAction(enabledIf: isValid) { _ in
12 | return Observable.create { observer in
13 |
14 | finishedSubject.onCompleted()
15 | observer.onCompleted()
16 |
17 | return Disposables.create()
18 | }
19 | }
20 |
21 | manualInvocation
22 | .subscribe(onNext: { [weak self] _ in
23 | self?.command.execute(Void())
24 | })
25 | .disposed(by: disposeBag)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Kiosk/Bid Fulfillment/KeypadContainerView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Foundation
3 | import RxSwift
4 | import Action
5 | import FLKAutoLayout
6 |
7 | //@IBDesignable
8 | class KeypadContainerView: UIView {
9 | fileprivate var keypad: KeypadView!
10 | fileprivate let viewModel = KeypadViewModel()
11 |
12 | var stringValue: Observable!
13 | var currencyValue: Observable!
14 | var deleteAction: CocoaAction!
15 | var resetAction: CocoaAction!
16 |
17 | override func prepareForInterfaceBuilder() {
18 | for subview in subviews { subview.removeFromSuperview() }
19 |
20 | let bundle = Bundle(for: type(of: self))
21 | let image = UIImage(named: "KeypadViewPreviewIB", in: bundle, compatibleWith: self.traitCollection)
22 | let imageView = UIImageView(frame: self.bounds)
23 | imageView.image = image
24 |
25 | self.addSubview(imageView)
26 | }
27 |
28 | override func awakeFromNib() {
29 | super.awakeFromNib()
30 |
31 | keypad = Bundle(for: type(of: self)).loadNibNamed("KeypadView", owner: self, options: nil)?.first as? KeypadView
32 | keypad.leftAction = viewModel.deleteAction
33 | keypad.rightAction = viewModel.clearAction
34 | keypad.keyAction = viewModel.addDigitAction
35 |
36 | currencyValue = viewModel.currencyValue.asObservable()
37 | stringValue = viewModel.stringValue.asObservable()
38 | deleteAction = viewModel.deleteAction
39 | resetAction = viewModel.clearAction
40 |
41 | self.addSubview(keypad)
42 |
43 | keypad.align(to: self)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Kiosk/Bid Fulfillment/KeypadView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import RxSwift
3 | import Action
4 |
5 | class KeypadView: UIView {
6 | var leftAction: CocoaAction? {
7 | didSet {
8 | self.leftButton.rx.action = leftAction
9 | }
10 | }
11 | var rightAction: CocoaAction? {
12 | didSet {
13 | self.rightButton.rx.action = rightAction
14 | }
15 | }
16 |
17 | var keyAction: Action?
18 |
19 | @IBOutlet fileprivate var keys: [Button]!
20 | @IBOutlet fileprivate var leftButton: Button!
21 | @IBOutlet fileprivate var rightButton: Button!
22 |
23 | @IBAction func keypadButtonTapped(_ sender: UIButton) {
24 | keyAction?.execute(sender.tag)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Kiosk/Bid Fulfillment/KeypadViewModel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Action
3 | import RxSwift
4 |
5 | let KeypadViewModelMaxIntegerValue: Currency = 10_000_000
6 |
7 | class KeypadViewModel: NSObject {
8 |
9 | //MARK: - Variables
10 |
11 | lazy var currencyValue = Variable(0)
12 |
13 | lazy var stringValue = Variable("")
14 |
15 | // MARK: - Actions
16 |
17 |
18 | lazy var deleteAction: CocoaAction = {
19 | return CocoaAction { [weak self] _ in
20 | self?.delete() ?? .empty()
21 | }
22 | }()
23 |
24 | lazy var clearAction: CocoaAction = {
25 | return CocoaAction { [weak self] _ in
26 | self?.clear() ?? .empty()
27 | }
28 | }()
29 |
30 | lazy var addDigitAction: Action = {
31 | let localSelf = self
32 | return Action { [weak localSelf] input in
33 | return localSelf?.addDigit(input) ?? .empty()
34 | }
35 | }()
36 | }
37 |
38 | private extension KeypadViewModel {
39 | func delete() -> Observable {
40 | return Observable.create { [weak self] observer in
41 | if let strongSelf = self {
42 | strongSelf.currencyValue.value = Currency(strongSelf.currencyValue.value / 10)
43 | if strongSelf.stringValue.value.isNotEmpty {
44 | var string = strongSelf.stringValue.value
45 | string.removeLast()
46 | strongSelf.stringValue.value = string
47 |
48 | }
49 | }
50 | observer.onCompleted()
51 | return Disposables.create()
52 | }
53 | }
54 |
55 | func clear() -> Observable {
56 | return Observable.create { [weak self] observer in
57 | self?.currencyValue.value = 0
58 | self?.stringValue.value = ""
59 | observer.onCompleted()
60 | return Disposables.create()
61 | }
62 | }
63 |
64 | func addDigit(_ input: Int) -> Observable {
65 | return Observable.create { [weak self] observer in
66 | if let strongSelf = self {
67 | let newValue = (10 * strongSelf.currencyValue.value) + Currency(input)
68 | if (newValue < KeypadViewModelMaxIntegerValue) {
69 | strongSelf.currencyValue.value = newValue
70 | }
71 | strongSelf.stringValue.value = "\(strongSelf.stringValue.value)\(input)"
72 | }
73 | observer.onCompleted()
74 | return Disposables.create()
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Kiosk/Bid Fulfillment/KeypadViewPreviewIB.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/Kiosk/Bid Fulfillment/KeypadViewPreviewIB.png
--------------------------------------------------------------------------------
/Kiosk/Bid Fulfillment/Models/NewUser.swift:
--------------------------------------------------------------------------------
1 | import RxSwift
2 |
3 | class NewUser {
4 | var email = Variable(nil)
5 | var password = Variable(nil)
6 | var phoneNumber = Variable(nil)
7 | var creditCardDigit = Variable(nil)
8 | var creditCardToken = Variable(nil)
9 | var creditCardName = Variable(nil)
10 | var creditCardType = Variable(nil)
11 | var zipCode = Variable(nil)
12 | var name = Variable(nil)
13 |
14 | var hasBeenRegistered = Variable(false)
15 |
16 | var swipedCreditCard = false
17 | }
--------------------------------------------------------------------------------
/Kiosk/Bid Fulfillment/PlaceBidNetworkModel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import RxSwift
3 | import Moya
4 | import SwiftyJSON
5 |
6 | let OutbidDomain = "Outbid"
7 |
8 | protocol PlaceBidNetworkModelType {
9 | var bidDetails: BidDetails { get }
10 |
11 | func bid(_ provider: AuthorizedNetworking) -> Observable
12 | }
13 |
14 | class PlaceBidNetworkModel: NSObject, PlaceBidNetworkModelType {
15 |
16 | let bidDetails: BidDetails
17 |
18 | init(bidDetails: BidDetails) {
19 | self.bidDetails = bidDetails
20 |
21 | super.init()
22 | }
23 |
24 | func bid(_ provider: AuthorizedNetworking) -> Observable {
25 | let saleArtwork = bidDetails.saleArtwork.value
26 |
27 | assert(saleArtwork.hasValue, "Sale artwork cannot nil at bidding stage.")
28 |
29 | let cents = (bidDetails.bidAmountCents.value?.currencyValue) ?? 0
30 | return bidOnSaleArtwork(saleArtwork!, bidAmountCents: String(cents), provider: provider)
31 | }
32 |
33 | fileprivate func bidOnSaleArtwork(_ saleArtwork: SaleArtwork, bidAmountCents: String, provider: AuthorizedNetworking) -> Observable {
34 | let bidEndpoint = ArtsyAuthenticatedAPI.placeABid(auctionID: saleArtwork.auctionID!, artworkID: saleArtwork.artwork.id, maxBidCents: bidAmountCents)
35 |
36 | let request = provider
37 | .request(bidEndpoint)
38 | .filterSuccessfulStatusCodes()
39 | .mapJSON()
40 | .mapTo(object: BidderPosition.self)
41 |
42 | return request
43 | .map { position in
44 | return position.id
45 | }.catchError { e -> Observable in
46 | // We've received an error. We're going to check to see if it's type is "param_error", which indicates we were outbid.
47 |
48 | guard let error = e as? MoyaError else { throw e }
49 | guard case .statusCode(let response) = error else { throw e }
50 |
51 |
52 | let json = try JSON(data: response.data)
53 |
54 | if let type = json["type"].string , type == "param_error" {
55 | throw NSError(domain: OutbidDomain, code: 0, userInfo: [NSUnderlyingErrorKey: error as NSError])
56 | } else {
57 | throw error
58 | }
59 | }
60 | .logError()
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/Kiosk/Bid Fulfillment/RegistrationEmailViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import RxSwift
3 | import RxOptional
4 |
5 | class RegistrationEmailViewController: UIViewController, RegistrationSubController, UITextFieldDelegate {
6 |
7 | @IBOutlet var emailTextField: TextField!
8 | @IBOutlet var confirmButton: ActionButton!
9 | var finished = PublishSubject()
10 |
11 | lazy var viewModel: GenericFormValidationViewModel = {
12 | let emailIsValid = self.emailTextField.rx.textInput.text.asObservable().replaceNilWith("").map(stringIsEmailAddress)
13 | return GenericFormValidationViewModel(isValid: emailIsValid, manualInvocation: self.emailTextField.rx_returnKey, finishedSubject: self.finished)
14 | }()
15 |
16 |
17 | fileprivate let _viewWillDisappear = PublishSubject()
18 | var viewWillDisappear: Observable {
19 | return self._viewWillDisappear.asObserver()
20 | }
21 |
22 | lazy var bidDetails: BidDetails! = { self.navigationController!.fulfillmentNav().bidDetails }()
23 |
24 | override func viewDidLoad() {
25 | super.viewDidLoad()
26 |
27 | emailTextField.text = bidDetails.newUser.email.value
28 | emailTextField.rx.textInput.text
29 | .asObservable()
30 | .takeUntil(viewWillDisappear)
31 | .bind(to: bidDetails.newUser.email)
32 | .disposed(by: rx.disposeBag)
33 |
34 | confirmButton.rx.action = viewModel.command
35 |
36 | emailTextField.becomeFirstResponder()
37 | }
38 |
39 | override func viewWillDisappear(_ animated: Bool) {
40 | super.viewWillDisappear(animated)
41 |
42 | _viewWillDisappear.onNext(Void())
43 | }
44 |
45 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
46 |
47 | // Allow delete
48 | if (string.isEmpty) { return true }
49 |
50 | // the API doesn't accept spaces
51 | return string != " "
52 | }
53 |
54 | class func instantiateFromStoryboard(_ storyboard: UIStoryboard) -> RegistrationEmailViewController {
55 | return storyboard.viewController(withID: .RegisterEmail) as! RegistrationEmailViewController
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Kiosk/Bid Fulfillment/RegistrationMobileViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import RxSwift
3 | import RxOptional
4 |
5 | class RegistrationMobileViewController: UIViewController, RegistrationSubController, UITextFieldDelegate {
6 |
7 | @IBOutlet var numberTextField: TextField!
8 | @IBOutlet var confirmButton: ActionButton!
9 | let finished = PublishSubject()
10 |
11 | lazy var viewModel: GenericFormValidationViewModel = {
12 | let numberIsValid = self.numberTextField.rx.text.asObservable().replaceNilWith("").map(isZeroLength).not()
13 | return GenericFormValidationViewModel(isValid: numberIsValid, manualInvocation: self.numberTextField.rx_returnKey, finishedSubject: self.finished)
14 | }()
15 |
16 | fileprivate let _viewWillDisappear = PublishSubject()
17 | var viewWillDisappear: Observable {
18 | return self._viewWillDisappear.asObserver()
19 | }
20 |
21 | lazy var bidDetails: BidDetails! = { self.navigationController!.fulfillmentNav().bidDetails }()
22 |
23 | override func viewDidLoad() {
24 | super.viewDidLoad()
25 |
26 | numberTextField.text = bidDetails.newUser.phoneNumber.value
27 | numberTextField
28 | .rx.text
29 | .asObservable()
30 | .takeUntil(viewWillDisappear)
31 | .bind(to: bidDetails.newUser.phoneNumber)
32 | .disposed(by: rx.disposeBag)
33 |
34 | confirmButton.rx.action = viewModel.command
35 |
36 | numberTextField.becomeFirstResponder()
37 | }
38 |
39 | override func viewWillDisappear(_ animated: Bool) {
40 | super.viewWillDisappear(animated)
41 |
42 | _viewWillDisappear.onNext(Void())
43 | }
44 |
45 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
46 |
47 | // Allow delete
48 | if string.isEmpty { return true }
49 |
50 | // the API doesn't accept chars
51 | let notNumberChars = CharacterSet.decimalDigits.inverted;
52 | return string.trimmingCharacters(in: notNumberChars).isNotEmpty
53 | }
54 |
55 | class func instantiateFromStoryboard(_ storyboard: UIStoryboard) -> RegistrationMobileViewController {
56 | return storyboard.viewController(withID: .RegisterMobile) as! RegistrationMobileViewController
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Kiosk/Bid Fulfillment/RegistrationPasswordViewModel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import RxSwift
3 | import Moya
4 | import Action
5 |
6 | protocol RegistrationPasswordViewModelType {
7 | var emailExists: Observable { get }
8 | var action: CocoaAction! { get }
9 |
10 | func userForgotPassword() -> Observable
11 | }
12 |
13 | class RegistrationPasswordViewModel: RegistrationPasswordViewModelType {
14 |
15 | fileprivate let password = Variable("")
16 |
17 | var action: CocoaAction!
18 | let provider: Networking
19 |
20 | let email: String
21 | let emailExists: Observable
22 |
23 | let disposeBag = DisposeBag()
24 |
25 | init(provider: Networking, password: Observable, execute: Observable, completed: PublishSubject, email: String) {
26 | self.provider = provider
27 | self.email = email
28 |
29 | let checkEmail = provider
30 | .request(ArtsyAPI.findExistingEmailRegistration(email: email))
31 | .map(responseIsOK)
32 | .share(replay: 1)
33 |
34 | emailExists = checkEmail
35 |
36 | password.bind(to: self.password).disposed(by: disposeBag)
37 |
38 | let password = self.password
39 |
40 | // Action takes nothing, is enabled if the password is valid, and does the following:
41 | // Check if the email exists, it tries to log in.
42 | // If it doesn't exist, then it does nothing.
43 | let action = CocoaAction(enabledIf: password.asObservable().map(isStringLengthAtLeast(length: 6))) { _ in
44 |
45 | return self.emailExists
46 | .flatMap { exists -> Observable in
47 | if exists {
48 | let endpoint: ArtsyAPI = ArtsyAPI.xAuth(email: email, password: password.value )
49 | return provider
50 | .request(endpoint)
51 | .filterSuccessfulStatusCodes()
52 | .map(void)
53 | } else {
54 | // Return a non-empty observable, so that the action sends something on its elements observable.
55 | return .just(Void())
56 | }
57 | }
58 | .do(onCompleted: {
59 | completed.onCompleted()
60 | })
61 | }
62 |
63 | self.action = action
64 |
65 | execute
66 | .subscribe { _ in
67 | action.execute(Void())
68 | }
69 | .disposed(by: disposeBag)
70 | }
71 |
72 | func userForgotPassword() -> Observable {
73 | let endpoint = ArtsyAPI.lostPasswordNotification(email: email)
74 | return provider.request(endpoint)
75 | .filterSuccessfulStatusCodes()
76 | .map(void)
77 | .do(onNext: { _ in
78 | logger.log("Sent forgot password request")
79 | })
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Kiosk/Bid Fulfillment/RegistrationPostalZipViewController.swift:
--------------------------------------------------------------------------------
1 | import RxSwift
2 | import RxOptional
3 |
4 | class RegistrationPostalZipViewController: UIViewController, RegistrationSubController {
5 | @IBOutlet var zipCodeTextField: TextField!
6 | @IBOutlet var confirmButton: ActionButton!
7 | let finished = PublishSubject()
8 |
9 | lazy var viewModel: GenericFormValidationViewModel = {
10 | let zipCodeIsValid = self.zipCodeTextField.rx.text.asObservable().replaceNilWith("").map(isZeroLength).not()
11 | return GenericFormValidationViewModel(isValid: zipCodeIsValid, manualInvocation: self.zipCodeTextField.rx_returnKey, finishedSubject: self.finished)
12 | }()
13 |
14 | fileprivate let _viewWillDisappear = PublishSubject()
15 | var viewWillDisappear: Observable {
16 | return self._viewWillDisappear.asObserver()
17 | }
18 |
19 | lazy var bidDetails: BidDetails! = { self.navigationController!.fulfillmentNav().bidDetails }()
20 |
21 | override func viewDidLoad() {
22 | super.viewDidLoad()
23 |
24 | zipCodeTextField.text = bidDetails.newUser.zipCode.value
25 |
26 | zipCodeTextField
27 | .rx.text
28 | .asObservable()
29 | .takeUntil(viewWillDisappear)
30 | .bind(to: bidDetails.newUser.zipCode)
31 | .disposed(by: rx.disposeBag)
32 |
33 | confirmButton.rx.action = viewModel.command
34 |
35 | zipCodeTextField.becomeFirstResponder()
36 | }
37 |
38 | override func viewWillDisappear(_ animated: Bool) {
39 | super.viewWillDisappear(animated)
40 |
41 | _viewWillDisappear.onNext(Void())
42 | }
43 |
44 | class func instantiateFromStoryboard(_ storyboard: UIStoryboard) -> RegistrationPostalZipViewController {
45 | return storyboard.viewController(withID: .RegisterPostalorZip) as! RegistrationPostalZipViewController
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Kiosk/Bid Fulfillment/StripeManager.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import RxSwift
3 | import Stripe
4 |
5 | protocol Tokenable {
6 | var tokenId: String { get }
7 | var creditCard: CreditCard? { get }
8 | }
9 |
10 | extension STPToken: Tokenable {
11 | var creditCard: CreditCard? {
12 | return self.card
13 | }
14 | }
15 |
16 | protocol Clientable {
17 | func kiosk_createToken(withCard card: STPCardParams, completion: ((Tokenable?, Error?) -> Void)?)
18 | }
19 | extension STPAPIClient: Clientable {
20 | func kiosk_createToken(withCard card: STPCardParams, completion: ((Tokenable?, Error?) -> Void)?) {
21 | self.createToken(withCard: card) { (token, error) in
22 | completion?(token, error)
23 | }
24 | }
25 | }
26 |
27 | class StripeManager: NSObject {
28 | var stripeClient: Clientable = STPAPIClient.shared()
29 |
30 | func registerCard(digits: String, month: UInt, year: UInt, securityCode: String, postalCode: String) -> Observable {
31 | let card = STPCardParams()
32 | card.number = digits
33 | card.expMonth = month
34 | card.expYear = year
35 | card.cvc = securityCode
36 | card.address.postalCode = postalCode
37 |
38 | return Observable.create { [weak self] observer in
39 | guard let me = self else {
40 | observer.onCompleted()
41 | return Disposables.create()
42 | }
43 |
44 | me.stripeClient.kiosk_createToken(withCard: card) { (token, error) in
45 | if let token = token {
46 | observer.onNext(token)
47 | observer.onCompleted()
48 | } else {
49 | observer.onError(error!)
50 | }
51 | }
52 |
53 | return Disposables.create()
54 | }
55 | }
56 |
57 | func stringIsCreditCard(_ cardNumber: String) -> Bool {
58 | return validateCreditCardNumber(cardNumber)
59 | }
60 | }
61 |
62 | extension STPCardBrand {
63 | var name: String? {
64 | switch self {
65 | case .visa:
66 | return "Visa"
67 | case .amex:
68 | return "American Express"
69 | case .masterCard:
70 | return "MasterCard"
71 | case .discover:
72 | return "Discover"
73 | case .JCB:
74 | return "JCB"
75 | case .dinersClub:
76 | return "Diners Club"
77 | default:
78 | return nil
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Kiosk/Bid Fulfillment/YourBiddingDetailsViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Artsy_UILabels
3 | import Artsy_UIButtons
4 | import RxCocoa
5 | import RxSwift
6 | import RxOptional
7 |
8 | class YourBiddingDetailsViewController: UIViewController {
9 |
10 | var provider: Networking!
11 |
12 | @IBOutlet dynamic var bidderNumberLabel: UILabel!
13 | @IBOutlet dynamic var pinNumberLabel: UILabel!
14 |
15 | @IBOutlet weak var bidderNumberTitleLabel: UILabel!
16 | @IBOutlet weak var bidderPinTitleLabel: UILabel!
17 | @IBOutlet weak var confirmationImageView: UIImageView!
18 | @IBOutlet weak var subtitleLabel: ARSerifLabel!
19 | @IBOutlet weak var bodyLabel: ARSerifLabel!
20 | @IBOutlet weak var notificationLabel: ARSerifLabel!
21 |
22 | var confirmationImage: UIImage?
23 |
24 | lazy var bidDetails: BidDetails! = { (self.navigationController as! FulfillmentNavigationController).bidDetails }()
25 |
26 | override func viewDidLoad() {
27 | super.viewDidLoad()
28 |
29 | [bidderNumberTitleLabel, bidderPinTitleLabel].forEach { $0?.backgroundColor = .clear }
30 |
31 | [notificationLabel, bidderNumberLabel, pinNumberLabel].forEach { $0.makeTransparent() }
32 | notificationLabel.setLineHeight(5)
33 | bodyLabel.setLineHeight(10)
34 |
35 | if let image = confirmationImage {
36 | confirmationImageView.image = image
37 | }
38 |
39 | bodyLabel?.makeSubstringsBold(["Bidder Number", "PIN"])
40 |
41 | bidDetails
42 | .paddleNumber
43 | .asObservable()
44 | .filterNilKeepOptional()
45 | .bind(to: bidderNumberLabel.rx.text)
46 | .disposed(by: rx.disposeBag)
47 |
48 | bidDetails
49 | .bidderPIN
50 | .asObservable()
51 | .filterNilKeepOptional()
52 | .bind(to: pinNumberLabel.rx.text)
53 | .disposed(by: rx.disposeBag)
54 | }
55 |
56 | @IBAction func confirmButtonTapped(_ sender: AnyObject) {
57 | fulfillmentContainer()?.closeFulfillmentModal()
58 | }
59 |
60 | class func instantiateFromStoryboard(_ storyboard: UIStoryboard) -> YourBiddingDetailsViewController {
61 | return storyboard.viewController(withID: .YourBidderDetails) as! YourBiddingDetailsViewController
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Kiosk/Bid Fulfillment/edit_button@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/Kiosk/Bid Fulfillment/edit_button@2x.png
--------------------------------------------------------------------------------
/Kiosk/Bid Fulfillment/toolbar_close@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/Kiosk/Bid Fulfillment/toolbar_close@2x.png
--------------------------------------------------------------------------------
/Kiosk/HelperFunctions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | typealias Currency = UInt64
4 |
5 | // Collection of stanardised mapping funtions for Rx work
6 |
7 | func stringIsEmailAddress(_ text: String) -> Bool {
8 | let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}"
9 | let testPredicate = NSPredicate(format:"SELF MATCHES %@", emailRegex)
10 | return testPredicate.evaluate(with: text)
11 | }
12 |
13 | fileprivate func createFormatter(_ currencySymbol: String) -> NumberFormatter {
14 | let newFormatter = NumberFormatter()
15 | newFormatter.locale = Locale.current
16 | newFormatter.currencySymbol = currencySymbol
17 | newFormatter.numberStyle = .currency
18 | newFormatter.maximumFractionDigits = 0
19 | newFormatter.alwaysShowsDecimalSeparator = false
20 | return newFormatter
21 | }
22 |
23 | func centsToPresentableDollarsString(_ cents: Currency, currencySymbol: String) -> String {
24 | let formatter = createFormatter(currencySymbol)
25 |
26 | guard let dollars = formatter.string(from: NSDecimalNumber(mantissa: cents, exponent: -2, isNegative: false)) else {
27 | return ""
28 | }
29 |
30 | return dollars
31 | }
32 |
33 | func isZeroLength(string: String) -> Bool {
34 | return string.isEmpty
35 | }
36 |
37 | func isStringLength(in range: Range) -> (String) -> Bool {
38 | return { string in
39 | return range.contains(string.count)
40 | }
41 | }
42 |
43 | func isStringOf(length: Int) -> (String) -> Bool {
44 | return { string in
45 | return string.count == length
46 | }
47 | }
48 |
49 | func isStringLengthAtLeast(length: Int) -> (String) -> Bool {
50 | return { string in
51 | return string.count >= length
52 | }
53 | }
54 |
55 | func isStringLength(oneOf lengths: [Int]) -> (String) -> Bool {
56 | return { string in
57 | return lengths.contains(string.count)
58 | }
59 | }
60 |
61 | // Useful for mapping an Observable into an Observable to hide details.
62 | func void(_: T) -> Void {
63 | return Void()
64 | }
65 |
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "ipad",
5 | "size" : "20x20",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "ipad",
10 | "size" : "20x20",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "ipad",
15 | "size" : "29x29",
16 | "scale" : "1x"
17 | },
18 | {
19 | "idiom" : "ipad",
20 | "size" : "29x29",
21 | "scale" : "2x"
22 | },
23 | {
24 | "idiom" : "ipad",
25 | "size" : "40x40",
26 | "scale" : "1x"
27 | },
28 | {
29 | "idiom" : "ipad",
30 | "size" : "40x40",
31 | "scale" : "2x"
32 | },
33 | {
34 | "size" : "76x76",
35 | "idiom" : "ipad",
36 | "filename" : "folioicon76.png",
37 | "scale" : "1x"
38 | },
39 | {
40 | "size" : "76x76",
41 | "idiom" : "ipad",
42 | "filename" : "folioicon152.png",
43 | "scale" : "2x"
44 | },
45 | {
46 | "size" : "83.5x83.5",
47 | "idiom" : "ipad",
48 | "filename" : "folioicon152-1.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "1024x1024",
53 | "idiom" : "ios-marketing",
54 | "filename" : "iTunesArtwork@2x.png",
55 | "scale" : "1x"
56 | }
57 | ],
58 | "info" : {
59 | "version" : 1,
60 | "author" : "xcode"
61 | }
62 | }
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/AppIcon.appiconset/folioicon152-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/Kiosk/Images.xcassets/AppIcon.appiconset/folioicon152-1.png
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/AppIcon.appiconset/folioicon152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/Kiosk/Images.xcassets/AppIcon.appiconset/folioicon152.png
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/AppIcon.appiconset/folioicon76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/Kiosk/Images.xcassets/AppIcon.appiconset/folioicon76.png
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/Kiosk/Images.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/ArtsyLogo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x",
10 | "filename" : "Logo@2x.png"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/ArtsyLogo.imageset/Logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/Kiosk/Images.xcassets/ArtsyLogo.imageset/Logo@2x.png
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/BackButtonBackground.imageset/BackButtonBackground@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/Kiosk/Images.xcassets/BackButtonBackground.imageset/BackButtonBackground@2x.png
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/BackButtonBackground.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "resizing" : {
9 | "mode" : "3-part-horizontal",
10 | "center" : {
11 | "mode" : "fill",
12 | "width" : 1
13 | },
14 | "capInsets" : {
15 | "right" : 6,
16 | "left" : 45
17 | }
18 | },
19 | "idiom" : "universal",
20 | "filename" : "BackButtonBackground@2x.png",
21 | "scale" : "2x"
22 | },
23 | {
24 | "idiom" : "universal",
25 | "scale" : "3x"
26 | }
27 | ],
28 | "info" : {
29 | "version" : 1,
30 | "author" : "xcode"
31 | }
32 | }
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/BidHighestBidder.imageset/BidHighestBidder@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/Kiosk/Images.xcassets/BidHighestBidder.imageset/BidHighestBidder@2x.png
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/BidHighestBidder.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x",
10 | "filename" : "BidHighestBidder@2x.png"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/BidNotHighestBidder.imageset/BidNotHighestBidder@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/Kiosk/Images.xcassets/BidNotHighestBidder.imageset/BidNotHighestBidder@2x.png
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/BidNotHighestBidder.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x",
10 | "filename" : "BidNotHighestBidder@2x.png"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/CardSwipe.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x",
6 | "filename" : "card-swipe-example.jpg"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/CardSwipe.imageset/card-swipe-example.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/Kiosk/Images.xcassets/CardSwipe.imageset/card-swipe-example.jpg
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/LaunchImage.launchimage/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "orientation" : "portrait",
5 | "idiom" : "ipad",
6 | "extent" : "full-screen",
7 | "minimum-system-version" : "7.0",
8 | "scale" : "1x"
9 | },
10 | {
11 | "orientation" : "landscape",
12 | "idiom" : "ipad",
13 | "extent" : "full-screen",
14 | "minimum-system-version" : "7.0",
15 | "scale" : "1x"
16 | },
17 | {
18 | "orientation" : "portrait",
19 | "idiom" : "ipad",
20 | "extent" : "full-screen",
21 | "minimum-system-version" : "7.0",
22 | "scale" : "2x"
23 | },
24 | {
25 | "orientation" : "landscape",
26 | "idiom" : "ipad",
27 | "extent" : "full-screen",
28 | "minimum-system-version" : "7.0",
29 | "scale" : "2x"
30 | }
31 | ],
32 | "info" : {
33 | "version" : 1,
34 | "author" : "xcode"
35 | }
36 | }
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/ProductionFlag.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x",
10 | "filename" : "ProductionFlag@2x.png"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/ProductionFlag.imageset/ProductionFlag@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/Kiosk/Images.xcassets/ProductionFlag.imageset/ProductionFlag@2x.png
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/StagingFlag.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x",
10 | "filename" : "StagingFlag@2x.png"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/StagingFlag.imageset/StagingFlag@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/Kiosk/Images.xcassets/StagingFlag.imageset/StagingFlag@2x.png
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/StubbingFlag.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "StagingFlag@2x.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/StubbingFlag.imageset/StagingFlag@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/Kiosk/Images.xcassets/StubbingFlag.imageset/StagingFlag@2x.png
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/xbtn_white.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x",
6 | "filename" : "xbtn_white.png"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x",
11 | "filename" : "xbtn_white@2x.png"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/xbtn_white.imageset/xbtn_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/Kiosk/Images.xcassets/xbtn_white.imageset/xbtn_white.png
--------------------------------------------------------------------------------
/Kiosk/Images.xcassets/xbtn_white.imageset/xbtn_white@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/Kiosk/Images.xcassets/xbtn_white.imageset/xbtn_white@2x.png
--------------------------------------------------------------------------------
/Kiosk/Observable+JSONAble.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Moya
3 | import RxSwift
4 |
5 | enum EidolonError: String {
6 | case couldNotParseJSON
7 | case notLoggedIn
8 | case missingData
9 | }
10 |
11 | extension EidolonError: Swift.Error { }
12 |
13 | extension Observable {
14 |
15 | typealias Dictionary = [String: AnyObject]
16 |
17 | /// Get given JSONified data, pass back objects
18 | func mapTo(object classType: B.Type) -> Observable {
19 | return self.map { json in
20 | guard let dict = json as? Dictionary else {
21 | throw EidolonError.couldNotParseJSON
22 | }
23 |
24 | return B.fromJSON(dict)
25 | }
26 | }
27 |
28 | /// Get given JSONified data, pass back objects as an array
29 | func mapTo(arrayOf classType: B.Type) -> Observable<[B]> {
30 | return self.map { json in
31 | guard let array = json as? [AnyObject] else {
32 | throw EidolonError.couldNotParseJSON
33 | }
34 |
35 | guard let dicts = array as? [Dictionary] else {
36 | throw EidolonError.couldNotParseJSON
37 | }
38 |
39 | return dicts.map { B.fromJSON($0) }
40 | }
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/Kiosk/Observable+Logging.swift:
--------------------------------------------------------------------------------
1 | import RxSwift
2 |
3 | extension Observable {
4 | func logError(prefix: String = "Error: ") -> Observable {
5 | return self.do(onError: { error in
6 | print("\(prefix)\(error)")
7 | })
8 | }
9 |
10 | func logServerError(message: String) -> Observable {
11 | return self.do(onError: { e in
12 | let error = e as NSError
13 | logger.log(message)
14 | logger.log("Error: \(error.localizedDescription). \n \(error.artsyServerError())")
15 | })
16 | }
17 |
18 | func logNext() -> Observable {
19 | return self.do(onNext: { element in
20 | print("\(element)")
21 | })
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Kiosk/Sale Artwork Details/ImageTiledDataSource.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import ARTiledImageView
3 |
4 | class TiledImageDataSourceWithImage: ARWebTiledImageDataSource {
5 | let image: Image
6 |
7 | init(image: Image) {
8 | self.image = image
9 | super.init()
10 |
11 | tileFormat = "jpg";
12 | tileBaseURL = URL(string: image.baseURL)
13 | tileSize = image.tileSize
14 | maxTiledHeight = image.maxTiledHeight
15 | maxTiledWidth = image.maxTiledWidth
16 | maxTileLevel = image.maxLevel
17 | minTileLevel = 11;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Kiosk/Sale Artwork Details/SaleArtworkZoomViewController.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import ARTiledImageView
3 |
4 | class SaleArtworkZoomViewController: UIViewController {
5 | var dataSource: TiledImageDataSourceWithImage!
6 | var saleArtwork: SaleArtwork!
7 | var tiledImageView: ARTiledImageScrollView!
8 |
9 | override func viewDidLoad() {
10 | super.viewDidLoad()
11 |
12 | let image = saleArtwork.artwork.defaultImage!
13 | dataSource = TiledImageDataSourceWithImage(image:image)
14 |
15 | let tiledView = ARTiledImageScrollView(frame:view.bounds)
16 | tiledView.decelerationRate = UIScrollViewDecelerationRateFast
17 | tiledView.showsHorizontalScrollIndicator = false
18 | tiledView.showsVerticalScrollIndicator = false
19 | tiledView.contentMode = .scaleAspectFit
20 | tiledView.dataSource = dataSource
21 | tiledView.backgroundImageURL = image.fullsizeURL() as URL!
22 |
23 | view.insertSubview(tiledView, at:0)
24 | tiledImageView = tiledView
25 | }
26 |
27 | override func viewWillAppear(_ animated: Bool) {
28 | super.viewWillAppear(animated)
29 |
30 | tiledImageView.zoom(toFit: false)
31 | }
32 |
33 | @IBAction func backButtonTapped(_ sender: AnyObject) {
34 | _ = self.navigationController?.popViewController(animated: true)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Kiosk/Sale Artwork Details/WhitespaceGobbler.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class WhitespaceGobbler: UIView {
4 | override init(frame: CGRect) {
5 | super.init(frame: frame)
6 | }
7 |
8 | required init?(coder aDecoder: NSCoder) {
9 | super.init(coder: aDecoder)
10 | }
11 |
12 | convenience init() {
13 | self.init(frame: CGRect.zero)
14 |
15 | setContentHuggingPriority(UILayoutPriority(rawValue: 50), for: .vertical)
16 | setContentHuggingPriority(UILayoutPriority(rawValue: 50), for: .horizontal)
17 | backgroundColor = .clear
18 | }
19 |
20 | override var intrinsicContentSize : CGSize {
21 | return CGSize.zero
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Kiosk/Storyboards/UIStoryboardExtensions.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UIStoryboard {
4 |
5 | class func auction() -> UIStoryboard {
6 | return UIStoryboard(name: StoryboardNames.Auction.rawValue, bundle: nil)
7 | }
8 |
9 | class func fulfillment() -> UIStoryboard {
10 | return UIStoryboard(name: StoryboardNames.Fulfillment.rawValue, bundle: nil)
11 | }
12 |
13 | func viewController(withID identifier:ViewControllerStoryboardIdentifier) -> UIViewController {
14 | return self.instantiateViewController(withIdentifier: identifier.rawValue)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Kiosk/Stubbed Responses/ActiveAuctions.json:
--------------------------------------------------------------------------------
1 | [{"profile":null,"_id":"527cffb2139b212d8a0004d5","id":"ici-live-auction","name":"ICI Live Auction","description":"","is_auction":true,"start_at":"2014-10-06T09:00:00+00:00","end_at":"2014-10-06T18:00:00+00:00","auction_state":"open","eligible_sale_artworks_count":5,"display_artist_list":false,"published":true,"created_at":"2013-11-08T15:13:54+00:00"}]
--------------------------------------------------------------------------------
/Kiosk/Stubbed Responses/Artist.json:
--------------------------------------------------------------------------------
1 | {"_id":"506b334b4466170002000759","id":"jessica-craig-martin","sortable_id":"craig-martin-jessica","name":"Jessica Craig-Martin","years":"born 1963","public":true,"gender":"female","hometown":"Hanover, New Hampshire","location":"New York","can_embed":false,"statement":"","biography":"","blurb":"Favoring harsh realism over glitz and glamor, Jessica Craig-Martin subverts the tradition of the society photograph. Having worked for publications such as _Vanity Fair_ and _W_, Martin is deeply entrenched in the world of high society parties. Her photographs begin as documents of these well-heeled events, but she crops out faces of partygoers, focusing on the minutiae they might rather remain unseen, such as sagging skin, wrinkle lines, or tans lines. Her detailed focus on blemishes and signifiers of wealth is coupled with high-contrast exposures that emphasize the superficiality of the glitterati in caustic color. Her work evokes the excess and abjection of [Nan Goldin](/artist/nan-goldin)'s photographs, styling the upper classes as absurd purveyors of artifice. “The camera is as cruel as the fashion and styling stunts it depicts are vain and ambitious,” wrote Glenn O’Brien of her work. “We see the socialite’s world as a House of Wax—a world so inhuman it attains the status of art.”","education":"","awards":"","publications":"","collections":"","soloexhibitions":"","groupexhibitions":"","image_rights":"","first":"Jessica","last":"Craig-Martin","middle":"","display_name":"","birthday":"1963","deathday":"","nationality":"American","published_artworks_count":15,"forsale_artworks_count":15,"artworks_count":17,"image_url":"http://static1.artsy.net/artist_images/52f6bdfe4a04f5d504f6c582/:version.jpg","image_versions":["square","tall","large","four_thirds"],"image_urls":{"square":"http://static1.artsy.net/artist_images/52f6bdfe4a04f5d504f6c582/square.jpg","tall":"http://static1.artsy.net/artist_images/52f6bdfe4a04f5d504f6c582/tall.jpg","large":"http://static1.artsy.net/artist_images/52f6bdfe4a04f5d504f6c582/large.jpg","four_thirds":"http://static1.artsy.net/artist_images/52f6bdfe4a04f5d504f6c582/four_thirds.jpg"},"original_height":null,"original_width":null,"follow_count":50,"alternate_names":null,"auction_lots_count":20}
--------------------------------------------------------------------------------
/Kiosk/Stubbed Responses/AuctionInfo.json:
--------------------------------------------------------------------------------
1 | {"profile":null,"_id":"527cffb2139b212d8a0004d5","id":"ici-live-auction","name":"ICI Live Auction","description":"","is_auction":true,"start_at":"2014-10-11T09:00:00+00:00","end_at":"2014-10-11T23:30:00+00:00","auction_state":"closed","eligible_sale_artworks_count":5,"display_artist_list":false,"published":true,"created_at":"2013-11-08T15:13:54+00:00"}
--------------------------------------------------------------------------------
/Kiosk/Stubbed Responses/CreateABidFail.json:
--------------------------------------------------------------------------------
1 | {"type":"param_error","message":"Please enter a bid higher than $14,000.","detail":{"base":["Please enter a bid higher than $14,000"]}}
--------------------------------------------------------------------------------
/Kiosk/Stubbed Responses/CreatePINForBidder.json:
--------------------------------------------------------------------------------
1 | {"sale":{"_id":"527cffb2139b212d8a0004d5","id":"ici-live-auction","name":"ICI Live Auction","auction_state":"open","published":true,"created_at":"2013-11-08T15:13:54+00:00"},"user":{"id":"542bf0d37261693f8c9e0500","type":"User","name":"djhfbsdjhgsdg","default_profile_id":"djhfbsdjhgsdg"},"id":"5432a1e372616931be050000","created_at":"2014-10-06T14:06:27+00:00","pin":"6510"}
--------------------------------------------------------------------------------
/Kiosk/Stubbed Responses/FindBidder.json:
--------------------------------------------------------------------------------
1 | {"sale":{"_id":"527cffb2139b212d8a0004d5","id":"ici-live-auction","name":"ICI Live Auction","auction_state":"open","published":true,"created_at":"2013-11-08T15:13:54+00:00"},"user":{"id":"542bf0d37261693f8c9e0500","type":"User","name":"djhfbsdjhgsdg","default_profile_id":"djhfbsdjhgsdg"},"id":"5432a1e372616931be050000","created_at":"2014-10-06T14:06:27+00:00","pin":"6520"}
--------------------------------------------------------------------------------
/Kiosk/Stubbed Responses/FindMyBidderRegistration.json:
--------------------------------------------------------------------------------
1 | [{"sale":{"_id":"54c7e8fa7261692b5acd0600","id":"los-angeles-modern-auctions-march-2015","name":"Los Angeles Modern Auctions - March 2015","is_auction":true,"auction_state":"open","published":true,"image_url":"https://d32dm0rphc51dk.cloudfront.net/BLv_dHIIVvShtDB8GCxFdg/:version.jpg","image_versions":["large_rectangle","source","square","wide"],"image_urls":{"large_rectangle":"https://d32dm0rphc51dk.cloudfront.net/BLv_dHIIVvShtDB8GCxFdg/large_rectangle.jpg","source":"https://d32dm0rphc51dk.cloudfront.net/BLv_dHIIVvShtDB8GCxFdg/source.jpg","square":"https://d32dm0rphc51dk.cloudfront.net/BLv_dHIIVvShtDB8GCxFdg/square.jpg","wide":"https://d32dm0rphc51dk.cloudfront.net/BLv_dHIIVvShtDB8GCxFdg/wide.jpg"},"original_width":null,"original_height":null,"created_at":"2015-01-27T19:37:30+00:00","currency":"USD"},"user":{"id":"56bcdc398b3b81698c000001","_id":"56bcdc398b3b81698c000001","name":"Testing Testing","default_profile_id":"user-56bcdc398b3b81698c000001"},"id":"56bcdc3a8b3b81698c00000f","created_by_admin":false,"created_at":"2016-02-11T19:08:42+00:00","pin":"7991"}]
--------------------------------------------------------------------------------
/Kiosk/Stubbed Responses/ForgotPassword.json:
--------------------------------------------------------------------------------
1 | {"status":"success"}
--------------------------------------------------------------------------------
/Kiosk/Stubbed Responses/Me.json:
--------------------------------------------------------------------------------
1 | {"location":{"id":"542bf0d37261693f8c9f0500","raw":null,"city":"","display":"","address":"","address_2":"","state":"","state_code":"","postal_code":"","country":"","coordinates":{"lng":-1.6167,"lat":53.7},"timezone":"Europe/London","summary":null},"lab_features":[],"id":"542bf0d37261693f8c9e0500","type":"User","name":"djhfbsdjhgsdg","default_profile_id":"djhfbsdjhgsdg","sale_profile_id":null,"email":"jlhdfbjsdfsdf@sdfjbsdjhfbs.com","phone":"1111","is_enabled":true,"has_password":true,"display_follow_tooltip":true,"display_filter_tooltip":true,"display_inquiry_tooltip":true,"display_favorites_dialog":true,"birthday":null,"gender":null,"artworks_per_year":null,"profession":null,"industry":null,"is_collector":false,"collector_since":null,"price_range":null,"notes":null,"welcome_email_sent_at":"2014-10-01T12:47:23+00:00","sign_in_count":5,"last_sign_in_at":"2014-10-06T12:04:18+00:00","last_sign_in_client":{"browser":"Paw"},"sign_up_user_agent":"Paw 2.0.9 (Macintosh; Mac OS X 10.10.0; en_GB)","receive_weekly_email":true,"receive_personalized_email":true,"receive_follow_users_email":true,"receive_offer_emails":true,"receive_personalized_show_email":true,"receive_personalized_artists_email":true,"publish_to_facebook":false,"receive_changelog_email":false,"collector_level":1,"likely_to_purchase":0,"timezone":"Europe/London","timezone_code":"BST","authentication_token":"2h-E5UZ5rmjz_7S","has_partner_access":false,"created_at":"2014-10-01T12:17:23+00:00","user_flags":{},"paddle_number":"565112","last_offer_email_time":null}
--------------------------------------------------------------------------------
/Kiosk/Stubbed Responses/MyBidPosition.json:
--------------------------------------------------------------------------------
1 | {"suggested_next_bid_cents":3800000,"highest_bid":null,"id":"5442d7117261696062370000","max_bid_amount_cents":3600000,"bid_max":false,"created_at":"2014-10-18T21:09:37+00:00","updated_at":"2014-10-18T21:09:37+00:00","active":true,"processed_at":"2014-10-18T21:09:37+00:00"}
--------------------------------------------------------------------------------
/Kiosk/Stubbed Responses/MyBidPositionsForAuctionArtwork.json:
--------------------------------------------------------------------------------
1 | [{"suggested_next_bid_cents":3800000,"highest_bid":null,"id":"5442d7117261696062370000","max_bid_amount_cents":3600000,"bid_max":false,"created_at":"2014-10-18T21:09:37+00:00","updated_at":"2014-10-18T21:09:37+00:00","active":true},{"suggested_next_bid_cents":3600000,"highest_bid":{"id":"5442d333776f72310a100000","amount_cents":3500000,"created_at":"2014-10-18T20:53:07+00:00"},"id":"5442d3327261696062350000","max_bid_amount_cents":3500000,"bid_max":false,"created_at":"2014-10-18T20:53:06+00:00","updated_at":"2014-10-18T20:53:06+00:00","active":true}]
--------------------------------------------------------------------------------
/Kiosk/Stubbed Responses/MyBiddersForAuction.json:
--------------------------------------------------------------------------------
1 | [{"sale":{"_id":"527cffb2139b212d8a0004d5","id":"ici-live-auction","name":"ICI Live Auction","auction_state":"open","published":true,"created_at":"2013-11-08T15:13:54+00:00"},"user":{"id":"542bf0d37261693f8c9e0500","type":"User","name":"djhfbsdjhgsdg","default_profile_id":"djhfbsdjhgsdg"},"id":"5432a1e372616931be050000","created_at":"2014-10-06T14:06:27+00:00","pin":"6520"}]
--------------------------------------------------------------------------------
/Kiosk/Stubbed Responses/MyCreditCards.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "brand": "Visa",
4 | "name": "MR O T THEROX",
5 | "deactivated_at": null,
6 | "id": "5192a13cbd0bbaa7c000114",
7 | "expiration_month": 11,
8 | "provider": "Balanced",
9 | "created_at": "2013-05-14T20:39:29.000Z",
10 | "last_digits": "9260",
11 | "expiration_year": 2014
12 | }
13 | ]
14 |
--------------------------------------------------------------------------------
/Kiosk/Stubbed Responses/Ping.json:
--------------------------------------------------------------------------------
1 | {
2 | "ping":"pong"
3 | }
--------------------------------------------------------------------------------
/Kiosk/Stubbed Responses/RegisterCard.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "brand": "Visa",
4 | "name": "MR O T THEROX",
5 | "deactivated_at": null,
6 | "id": "5192a13cbd0bbaa7c000114",
7 | "expiration_month": 11,
8 | "provider": "Balanced",
9 | "created_at": "2013-05-14T20:39:29.000Z",
10 | "last_digits": "9260",
11 | "expiration_year": 2014
12 | }
13 | ]
14 |
--------------------------------------------------------------------------------
/Kiosk/Stubbed Responses/RegisterToBid.json:
--------------------------------------------------------------------------------
1 | {
2 | "sale": {
3 | "_id": "527cffb2139b212d8a0004d5",
4 | "id": "ici-live-auction",
5 | "name": "ICI Live Auction",
6 | "auction_state": "open",
7 | "published": true,
8 | "created_at": "2013-11-08T15:13:54+00:00"
9 | },
10 | "user": {
11 | "id": "542bf0d37261693f8c9e0500",
12 | "type": "User",
13 | "name": "djhfbsdjhgsdgd",
14 | "default_profile_id": "djhfbsdjhgsdg",
15 | "price_range": "-1:1000000000000"
16 | },
17 | "id": "5432a1e372616931be050000",
18 | "created_at": "2014-10-06T14:06:27+00:00",
19 | "pin": "3989"
20 | }
--------------------------------------------------------------------------------
/Kiosk/Stubbed Responses/SystemTime.json:
--------------------------------------------------------------------------------
1 | {"time":"2014-09-24 19:22:24 UTC","day":24,"wday":3,"month":9,"year":2014,"hour":19,"min":22,"sec":24,"dst":false,"unix":1411586544,"utc_offset":0,"zone":"UTC","iso8601":"2422-09-24T19:22:24Z"}
--------------------------------------------------------------------------------
/Kiosk/Stubbed Responses/XApp.json:
--------------------------------------------------------------------------------
1 | {"xapp_token":"STUBBED TOKEN!","expires_in":"2024-09-19T12:22:21.570Z"}
--------------------------------------------------------------------------------
/Kiosk/Stubbed Responses/XAuth.json:
--------------------------------------------------------------------------------
1 | {
2 | "access_token": "sPH1d_f1p92S_DD8ChuvwxrH1R33Rcpaj-x8rNLRqrMv5MuobcQf6TfJpmoe1XpbC_-125unn3S2S_mJylZ-seWMlrAputmSG9McRNrY0G-YZfqACTk3kV4zDUADRRlPZzI-abifvUjYg_k-Dz4NkATi7gpISSxRUS6poQMXmhdNd_uTdjW5tZRo8SB-8we4QE7L0lfF5YoOhazEn6NPDHm8q2muC1XRkKn_eyyzZvbSQPFPH6gQiNhcfV-iSGxN2H4469eghn0THwIEuNcToBkTwE926_bAnWwoZ749BdR2",
3 | "expires_in": "2039-09-28T16:06:45Z"
4 | }
--------------------------------------------------------------------------------
/Kiosk/Supporting Files/BridgingHeader.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | #import "PodsBridgingHeader.h"
5 |
6 | #import "StubResponses.h"
7 | #import "CreditCardValidation.h"
8 | #import "KioskDateFormatter.h"
9 |
--------------------------------------------------------------------------------
/Kiosk/Supporting Files/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | Kiosk β
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 5.9.3
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | 2018.02.07
25 | LSRequiresIPhoneOS
26 |
27 | NSAppTransportSecurity
28 |
29 | NSAllowsArbitraryLoads
30 |
31 |
32 | NSBluetoothPeripheralUsageDescription
33 | Credit Card Reader
34 | NSMicrophoneUsageDescription
35 | Credit Card Reader
36 | UIAppFonts
37 |
38 | AGaramondPro-BoldItalic.otf
39 | AGaramondPro-Bold.otf
40 | AGaramondPro-Italic.otf
41 | AGaramondPro-Regular.otf
42 | AVG65lig.otf
43 | AGaramondPro-Semibold.otf
44 |
45 | UIRequiredDeviceCapabilities
46 |
47 | armv7
48 |
49 | UIStatusBarHidden
50 |
51 | UISupportedExternalAccessoryProtocols
52 |
53 | com.miura.shuttle
54 | com.cardflight.bold
55 |
56 | UISupportedInterfaceOrientations~ipad
57 |
58 | UIInterfaceOrientationLandscapeLeft
59 | UIInterfaceOrientationLandscapeRight
60 |
61 | UIViewControllerBasedStatusBarAppearance
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/Kiosk/Supporting Files/PodsBridgingHeader.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | //#import
5 | #import
6 | #import
7 |
8 | #import
9 |
10 | #import
11 |
12 | #import
13 |
14 | #import
15 | #import
16 |
17 | // Fonts can come from one of two Pods, but each has the same module/header name.
18 | #import
19 |
20 | #import
21 | #import
22 |
23 | #import
24 | #import
25 | #import
26 | #import
27 |
28 | #import
29 |
--------------------------------------------------------------------------------
/Kiosk/UIKit+Rx.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import RxSwift
3 | import RxCocoa
4 |
5 | extension UIView {
6 | public var rx_hidden: AnyObserver {
7 | return AnyObserver { [weak self] event in
8 | MainScheduler.ensureExecutingOnScheduler()
9 |
10 | switch event {
11 | case .next(let value):
12 | self?.isHidden = value
13 | case .error(let error):
14 | bindingErrorToInterface(error)
15 | break
16 | case .completed:
17 | break
18 | }
19 | }
20 | }
21 | }
22 |
23 | extension UITextField {
24 | var rx_returnKey: Observable {
25 | return self.rx.controlEvent(.editingDidEndOnExit).takeUntil(rx.deallocated)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Kiosk/UILabel+Fonts.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UILabel {
4 | func makeSubstringsBold(_ text: [String]) {
5 | text.forEach { self.makeSubstringBold($0) }
6 | }
7 |
8 | func makeSubstringBold(_ boldText: String) {
9 | let attributedText = self.attributedText!.mutableCopy() as! NSMutableAttributedString
10 |
11 | let range = ((self.text ?? "") as NSString).range(of: boldText)
12 | if range.location != NSNotFound {
13 | attributedText.setAttributes([NSAttributedStringKey.font: UIFont.serifSemiBoldFont(withSize: self.font.pointSize)], range: range)
14 | }
15 |
16 | self.attributedText = attributedText
17 | }
18 |
19 | func makeSubstringsItalic(_ text: [String]) {
20 | text.forEach { self.makeSubstringItalic($0) }
21 | }
22 |
23 | func makeSubstringItalic(_ italicText: String) {
24 | let attributedText = self.attributedText!.mutableCopy() as! NSMutableAttributedString
25 |
26 | let range = ((self.text ?? "") as NSString).range(of: italicText)
27 | if range.location != NSNotFound {
28 | attributedText.setAttributes([NSAttributedStringKey.font: UIFont.serifItalicFont(withSize: self.font.pointSize)], range: range)
29 | }
30 |
31 | self.attributedText = attributedText
32 | }
33 |
34 | func setLineHeight(_ lineHeight: Int) {
35 | let displayText = text ?? ""
36 | let attributedString = self.attributedText!.mutableCopy() as! NSMutableAttributedString
37 | let paragraphStyle = NSMutableParagraphStyle()
38 | paragraphStyle.lineSpacing = CGFloat(lineHeight)
39 | paragraphStyle.alignment = textAlignment
40 | attributedString.addAttribute(NSAttributedStringKey.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, displayText.count))
41 |
42 | attributedText = attributedString
43 | }
44 |
45 | func makeTransparent() {
46 | isOpaque = false
47 | backgroundColor = .clear
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Kiosk/UIView+LongPressDisplayMessage.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import RxSwift
3 |
4 | private func alertController(_ message: String, title: String) -> UIAlertController {
5 | let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
6 |
7 | alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
8 |
9 | return alertController
10 | }
11 |
12 | extension UIView {
13 | typealias PresentAlertClosure = (_ alertController: UIAlertController) -> Void
14 |
15 | func presentOnLongPress(_ message: String, title: String, closure: @escaping PresentAlertClosure) {
16 | let recognizer = UILongPressGestureRecognizer()
17 |
18 | recognizer
19 | .rx.event
20 | .subscribe(onNext: { _ in
21 | closure(alertController(message, title: title))
22 | })
23 | .disposed(by: rx.disposeBag)
24 |
25 | isUserInteractionEnabled = true
26 | addGestureRecognizer(recognizer)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Kiosk/UIViewController+Bidding.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import ARAnalytics
3 |
4 | extension UIViewController {
5 | func bid(auctionID: String, saleArtwork: SaleArtwork, allowAnimations: Bool, provider: Networking) {
6 | ARAnalytics.event("Bid Button Tapped", withProperties: ["id": saleArtwork.artwork.id])
7 |
8 | let storyboard = UIStoryboard.fulfillment()
9 | let containerController = storyboard.instantiateInitialViewController() as! FulfillmentContainerViewController
10 | containerController.allowAnimations = allowAnimations
11 |
12 | if let internalNav:FulfillmentNavigationController = containerController.internalNavigationController() {
13 | internalNav.auctionID = auctionID
14 | internalNav.bidDetails.saleArtwork = saleArtwork
15 | internalNav.provider = provider
16 | }
17 |
18 | // Present the VC, then once it's ready trigger it's own showing animations
19 | appDelegate().appViewController.present(containerController, animated: false) {
20 | containerController.viewDidAppearAnimation(containerController.allowAnimations)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/KioskTests/App/ArtsyProviderTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | import RxSwift
4 | @testable
5 | import Kiosk
6 | import Moya
7 |
8 | class ArtsyProviderTests: QuickSpec {
9 | override func spec() {
10 | let fakeEndpointsClosure = { (target: ArtsyAPI) -> Endpoint in
11 | return Endpoint(url: url(target), sampleResponseClosure: {.networkResponse(200, target.sampleData)}, method: target.method, task: target.task, httpHeaderFields: nil)
12 | }
13 |
14 | var fakeOnline: PublishSubject!
15 | var subject: Networking!
16 | var defaults: UserDefaults!
17 |
18 | beforeEach {
19 | fakeOnline = PublishSubject()
20 | subject = Networking(provider: OnlineProvider(endpointClosure: fakeEndpointsClosure, stubClosure: MoyaProvider.immediatelyStub, online: fakeOnline.asObservable()))
21 |
22 | // We fake our defaults to avoid actually hitting the network
23 | defaults = UserDefaults()
24 | defaults.set(NSDate.distantFuture, forKey: "TokenExpiry")
25 | defaults.set("Some key", forKey: "TokenKey")
26 | }
27 |
28 | it ("waits for the internet to happen before continuing with network operations") {
29 | var called = false
30 |
31 | let disposeBag = DisposeBag()
32 | subject.request(ArtsyAPI.ping, defaults: defaults).subscribe(onNext: { _ in
33 | called = true
34 | }).disposed(by: disposeBag)
35 |
36 | expect(called) == false
37 |
38 | // Fake getting online
39 | fakeOnline.onNext(true)
40 |
41 | expect(called) == true
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/KioskTests/App/LoggerTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | @testable
4 | import Kiosk
5 |
6 | func logPath() -> URL {
7 | let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last
8 | return docs!.appendingPathComponent("logger.txt")
9 | }
10 |
11 | class LoggerTests: QuickSpec {
12 | override func spec() {
13 | it("amends contents of logging file") {
14 | let testString = "Nobody has margaritas with pizza."
15 | let logger = Logger(destination: logPath())
16 |
17 | logger.log(testString)
18 |
19 | let fileContents = try! String(contentsOf: logPath(), encoding: .utf8)
20 |
21 | expect(fileContents).to(contain(testString))
22 | }
23 |
24 | afterEach {
25 | try! FileManager.default.removeItem(at: logPath())
26 | return
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/KioskTests/AppViewControllerTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | import Nimble_Snapshots
4 | import RxSwift
5 | @testable
6 | import Kiosk
7 |
8 | class AppViewControllerTests: QuickSpec {
9 | override func spec() {
10 |
11 | it("looks right offline") {
12 | let subject = UIStoryboard.auction().viewController(withID: .NoInternetConnection) as UIViewController
13 | subject.loadViewProgrammatically()
14 | subject.view.backgroundColor = UIColor.black
15 | expect(subject).to(haveValidSnapshot())
16 | }
17 |
18 | describe("view") {
19 | var subject: AppViewController!
20 | var fakeReachability: Variable!
21 |
22 | beforeEach {
23 | subject = AppViewController.instantiate(from: auctionStoryboard)
24 | subject.provider = Networking.newStubbingNetworking()
25 | fakeReachability = Variable(true)
26 |
27 | subject.reachability = fakeReachability.asObservable()
28 | subject.apiPinger = Observable.just(true).take(1)
29 | }
30 |
31 | it("shows the offlineBlockingView when offline is true"){
32 | subject.loadViewProgrammatically()
33 |
34 | subject.offlineBlockingView.isHidden = false
35 |
36 | fakeReachability.value = true
37 | expect(subject.offlineBlockingView.isHidden) == true
38 | }
39 |
40 | it("hides the offlineBlockingView when offline is false"){
41 | subject.loadViewProgrammatically()
42 |
43 | fakeReachability.value = true
44 | expect(subject.offlineBlockingView.isHidden) == true
45 |
46 |
47 | fakeReachability.value = false
48 | expect(subject.offlineBlockingView.isHidden) == false
49 |
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/KioskTests/Bid Fulfillment/BidderNetworkModelTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | import RxNimble
4 | import RxSwift
5 | import Moya
6 | @testable
7 | import Kiosk
8 |
9 | class BidderNetworkModelTests: QuickSpec {
10 | override func spec() {
11 | var bidDetails: BidDetails!
12 | var subject: BidderNetworkModel!
13 | var disposeBag: DisposeBag!
14 |
15 | beforeEach {
16 | bidDetails = testBidDetails()
17 | bidDetails.newUser.email.value = "asdf@asdf.asdf"
18 | bidDetails.newUser.phoneNumber.value = "12345678"
19 | bidDetails.newUser.zipCode.value = "10013"
20 | subject = BidderNetworkModel(provider: Networking.newStubbingNetworking(), bidDetails: bidDetails)
21 | disposeBag = DisposeBag()
22 | }
23 |
24 | it("matches hasBeenRegistered is false") {
25 | expect(subject.createdNewUser).first == false
26 | }
27 |
28 | it("matches hasBeenRegistered is true") {
29 | bidDetails.newUser.hasBeenRegistered.value = true
30 | expect(try! subject.createdNewUser.toBlocking().first()) == true
31 | }
32 |
33 | it("sends a value even if not adding a card") {
34 | waitUntil { done in
35 | subject
36 | .createOrGetBidder()
37 | .subscribe(onNext: { _ in
38 | done()
39 | })
40 | .disposed(by: disposeBag)
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/KioskTests/Bid Fulfillment/ConfirmYourBidArtsyLoginViewControllerTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | import Nimble_Snapshots
4 | @testable
5 | import Kiosk
6 |
7 | class ConfirmYourBidArtsyLoginViewControllerTests: QuickSpec {
8 | override func spec() {
9 |
10 | it("looks right by default") {
11 | let subject = ConfirmYourBidArtsyLoginViewController.instantiateFromStoryboard(fulfillmentStoryboard).wrapInFulfillmentNav()
12 | subject.loadViewProgrammatically()
13 |
14 | // Highlighting of the text field (as it becomes first responder) is inconsistent without this line.
15 | subject.view.drawHierarchy(in: CGRect.zero, afterScreenUpdates: true)
16 |
17 | // There's some strange button enabled state animation that's messing with the tests. Adding a tolance.
18 | expect(subject).to(haveValidSnapshot(usesDrawRect: true, tolerance: 0.1))
19 | }
20 |
21 | pending("looks right with an invalid password") {
22 | // TODO:
23 | }
24 |
25 | pending("looks right with a valid password") {
26 | // TODO:
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/KioskTests/Bid Fulfillment/ConfirmYourBidPINViewControllerTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | @testable
4 | import Kiosk
5 | import Moya
6 | import RxSwift
7 | import Nimble_Snapshots
8 |
9 | class ConfirmYourBidPINViewControllerTests: QuickSpec {
10 | override func spec() {
11 |
12 | it("looks right by default") {
13 | let subject = testConfirmYourBidPINViewController()
14 | subject.loadViewProgrammatically()
15 | expect(subject) == snapshot()
16 | }
17 |
18 | it("reacts to keypad inputs with the string") {
19 | let customKeySubject = PublishSubject()
20 | let subject = testConfirmYourBidPINViewController()
21 | subject.pin = customKeySubject.asObservable()
22 | subject.loadViewProgrammatically()
23 |
24 | customKeySubject.onNext("2344");
25 | expect(subject.pinTextField.text) == "2344"
26 | }
27 |
28 | it("reacts to keypad inputs with the string") {
29 | let customKeySubject = PublishSubject()
30 |
31 | let subject = testConfirmYourBidPINViewController()
32 | subject.pin = customKeySubject
33 |
34 | subject.loadViewProgrammatically()
35 |
36 | customKeySubject.onNext("2");
37 | expect(subject.pinTextField.text) == "2"
38 | }
39 |
40 | it("reacts to keypad inputs with the string") {
41 | let customKeySubject = PublishSubject()
42 |
43 | let subject = testConfirmYourBidPINViewController()
44 | subject.pin = customKeySubject;
45 |
46 | subject.loadViewProgrammatically()
47 |
48 | customKeySubject.onNext("222");
49 | expect(subject.pinTextField.text) == "222"
50 | }
51 | }
52 | }
53 |
54 | func testConfirmYourBidPINViewController() -> ConfirmYourBidPINViewController {
55 | let controller = ConfirmYourBidPINViewController.instantiateFromStoryboard(fulfillmentStoryboard).wrapInFulfillmentNav() as! ConfirmYourBidPINViewController
56 | controller.provider = Networking.newStubbingNetworking()
57 | return controller
58 | }
59 |
--------------------------------------------------------------------------------
/KioskTests/Bid Fulfillment/ConfirmYourBidViewControllerTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | import Nimble_Snapshots
4 | import RxSwift
5 | @testable
6 | import Kiosk
7 |
8 | class ConfirmYourBidViewControllerTests: QuickSpec {
9 | override func spec() {
10 | var subject: ConfirmYourBidViewController!
11 |
12 | beforeEach {
13 | subject = ConfirmYourBidViewController.instantiateFromStoryboard(fulfillmentStoryboard).wrapInFulfillmentNav() as! ConfirmYourBidViewController
14 | }
15 |
16 | pending("looks right by default") {
17 | subject.loadViewProgrammatically()
18 | expect(subject) == snapshot("default")
19 | }
20 |
21 | it("shows keypad buttons") {
22 | let keypadSubject = Variable("")
23 | subject.number = keypadSubject.asObservable()
24 |
25 | subject.loadViewProgrammatically()
26 | keypadSubject.value = "3"
27 |
28 | expect(subject.numberAmountTextField.text) == "3"
29 | }
30 |
31 | pending("changes enter button to enabled") {
32 | let keypadSubject = Variable("")
33 | subject.number = keypadSubject.asObservable()
34 |
35 | subject.loadViewProgrammatically()
36 |
37 | expect(subject.enterButton.isEnabled) == false
38 | keypadSubject.value = "3"
39 | expect(subject.enterButton.isEnabled) == true
40 | }
41 |
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/KioskTests/Bid Fulfillment/FulfillmentContainerViewControllerTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 |
4 | class FulfillmentContainerViewControllerTests: QuickSpec {
5 | override func spec() {
6 |
7 | pending("dismisses itself when closeModalTapped is called") {
8 | }
9 |
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/KioskTests/Bid Fulfillment/FulfillmentNavigationControllerTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | @testable
4 | import Kiosk
5 | import Moya
6 | import RxSwift
7 |
8 | class FulfillmentNavigationControllerTests: QuickSpec {
9 | override func spec() {
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/KioskTests/Bid Fulfillment/GenericFormValidationViewModelTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | import RxNimble
4 | import RxSwift
5 | @testable
6 | import Kiosk
7 |
8 | class GenericFormValidationViewModelTests: QuickSpec {
9 | override func spec() {
10 | var validSubject: Observable!
11 | var disposeBag: DisposeBag!
12 |
13 | beforeEach {
14 | validSubject = Observable.just(true)
15 | disposeBag = DisposeBag()
16 | }
17 |
18 | it("executes command when manual sends") {
19 | var completed = false
20 |
21 | let invocation = PublishSubject()
22 |
23 | let subject = GenericFormValidationViewModel(isValid: validSubject, manualInvocation: invocation, finishedSubject: PublishSubject())
24 |
25 | subject.command.executing.take(1).subscribe(onNext: { _ in
26 | completed = true
27 | }).disposed(by: disposeBag)
28 |
29 | invocation.onNext(Void())
30 |
31 | expect(completed).toEventually( beTrue() )
32 | }
33 |
34 | it("sends completed on finishedSubject when command is executed") {
35 | var completed = false
36 |
37 | let invocation = PublishSubject()
38 | let finishedSubject = PublishSubject()
39 |
40 | finishedSubject.subscribe(onCompleted: {
41 | completed = true
42 | }).disposed(by: disposeBag)
43 |
44 | let subject = GenericFormValidationViewModel(isValid: validSubject, manualInvocation: invocation, finishedSubject: finishedSubject)
45 |
46 | subject.command.execute(Void())
47 |
48 | expect(completed).toEventually( beTrue() )
49 | }
50 |
51 | it("uses the isValid for the command enabledness") {
52 | let validSubject = PublishSubject()
53 |
54 | let subject = GenericFormValidationViewModel(isValid: validSubject, manualInvocation: Observable.empty(), finishedSubject: PublishSubject())
55 |
56 | validSubject.onNext(false)
57 | expect(subject.command.enabled).first.toEventually( equal(false) )
58 |
59 | validSubject.onNext(true)
60 | expect(subject.command.enabled).first.toEventually( equal(true) )
61 |
62 | validSubject.onNext(false)
63 | expect(subject.command.enabled).first.toEventually( equal(false) )
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/KioskTests/Bid Fulfillment/RegistrationEmailViewControllerTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | @testable
4 | import Kiosk
5 | import Nimble_Snapshots
6 |
7 | class RegistrationEmailViewControllerTests: QuickSpec {
8 | override func spec() {
9 | it("looks right by default") {
10 | let subject = RegistrationEmailViewController.instantiateFromStoryboard(fulfillmentStoryboard)
11 | subject.bidDetails = testBidDetails()
12 | expect(subject).to( haveValidSnapshot(usesDrawRect: true) )
13 | }
14 |
15 | it("looks right with existing email") {
16 | let subject = RegistrationEmailViewController.instantiateFromStoryboard(fulfillmentStoryboard)
17 | subject.bidDetails = testBidDetails()
18 | subject.bidDetails.newUser.email.value = "test@example.com"
19 | expect(subject).to( haveValidSnapshot(usesDrawRect: true) )
20 | }
21 |
22 | it("unbinds bidDetails on viewWillDisappear:") {
23 | let runLifecycleOfViewController = { (bidDetails: BidDetails) -> RegistrationEmailViewController in
24 | let subject = RegistrationEmailViewController.instantiateFromStoryboard(fulfillmentStoryboard)
25 | subject.bidDetails = bidDetails
26 | subject.loadViewProgrammatically()
27 | subject.viewWillDisappear(false)
28 | return subject
29 | }
30 |
31 | let bidDetails = testBidDetails()
32 | _ = runLifecycleOfViewController(bidDetails)
33 |
34 | expect { runLifecycleOfViewController(bidDetails) }.toNot( raiseException() )
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/KioskTests/Bid Fulfillment/RegistrationMobileViewControllerTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | @testable
4 | import Kiosk
5 | import RxSwift
6 | import Nimble_Snapshots
7 | import Moya
8 |
9 | class RegistrationMobileViewControllerTests: QuickSpec {
10 | override func spec() {
11 | it("looks right by default") {
12 | let subject = RegistrationMobileViewController.instantiateFromStoryboard(fulfillmentStoryboard)
13 | subject.bidDetails = testBidDetails()
14 | expect(subject).to( haveValidSnapshot() )
15 | }
16 |
17 | it("looks right with existing mobile") {
18 | let subject = RegistrationMobileViewController.instantiateFromStoryboard(fulfillmentStoryboard)
19 | subject.bidDetails = testBidDetails()
20 | subject.bidDetails.newUser.phoneNumber.value = "1234567890"
21 | expect(subject).to( haveValidSnapshot() )
22 | }
23 |
24 | it("unbinds bidDetails on viewWillDisappear:") {
25 | let runLifecycleOfViewController = { (bidDetails: BidDetails) -> RegistrationMobileViewController in
26 | let subject = RegistrationMobileViewController.instantiateFromStoryboard(fulfillmentStoryboard)
27 | subject.bidDetails = bidDetails
28 | subject.loadViewProgrammatically()
29 | subject.viewWillDisappear(false)
30 | return subject
31 | }
32 |
33 | let bidDetails = testBidDetails()
34 | _ = runLifecycleOfViewController(bidDetails)
35 |
36 | expect { runLifecycleOfViewController(bidDetails) }.toNot( raiseException() )
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/KioskTests/Bid Fulfillment/RegistrationPasswordViewControllerTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | @testable
4 | import Kiosk
5 | import RxSwift
6 | import Nimble_Snapshots
7 | import Action
8 | import Moya
9 |
10 | class RegistrationPasswordViewControllerTests: QuickSpec {
11 |
12 | func testSubject(emailExists: Bool = false) -> RegistrationPasswordViewController {
13 |
14 | class TestViewModel: RegistrationPasswordViewModelType {
15 |
16 | var emailExists: Observable
17 | var action: CocoaAction! = emptyAction()
18 |
19 | init (emailExists: Bool = false) {
20 | self.emailExists = Observable.just(emailExists)
21 | }
22 |
23 | func userForgotPassword() -> Observable {
24 | return Observable.empty()
25 | }
26 | }
27 |
28 | let subject = RegistrationPasswordViewController.instantiateFromStoryboard(fulfillmentStoryboard)
29 | subject.bidDetails = testBidDetails()
30 | subject.viewModel = TestViewModel(emailExists: emailExists)
31 | return subject
32 | }
33 |
34 | override func spec() {
35 | it("looks right by default") {
36 | let subject = self.testSubject()
37 | expect(subject).to( haveValidSnapshot() )
38 | }
39 |
40 | it("looks right with an existing email") {
41 | let subject = self.testSubject(emailExists: true)
42 | expect(subject).to( haveValidSnapshot() )
43 | }
44 |
45 | it("looks right with a valid password") {
46 | let subject = self.testSubject()
47 | subject.loadViewProgrammatically()
48 | subject.passwordTextField.text = "password"
49 | expect(subject).to( haveValidSnapshot() )
50 | }
51 |
52 | it("looks right with an invalid password") {
53 | let subject = self.testSubject()
54 | subject.loadViewProgrammatically()
55 | subject.passwordTextField.text = "short"
56 | expect(subject).to( haveValidSnapshot() )
57 | }
58 |
59 | it("unbinds bidDetails on viewWillDisappear:") {
60 | let runLifecycleOfViewController = { (bidDetails: BidDetails) -> RegistrationPasswordViewController in
61 | let subject = RegistrationPasswordViewController.instantiateFromStoryboard(fulfillmentStoryboard)
62 | subject.provider = Networking.newStubbingNetworking()
63 | subject.bidDetails = bidDetails
64 | subject.loadViewProgrammatically()
65 | subject.viewWillDisappear(false)
66 | return subject
67 | }
68 |
69 | let bidDetails = testBidDetails()
70 | _ = runLifecycleOfViewController(bidDetails)
71 |
72 | expect { runLifecycleOfViewController(bidDetails) }.toNot( raiseException() )
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/KioskTests/Bid Fulfillment/RegistrationPostalZipViewControllerTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | @testable
4 | import Kiosk
5 | import Nimble_Snapshots
6 |
7 | class RegistrationPostalViewControllerTests: QuickSpec {
8 | override func spec() {
9 | it("looks right by default") {
10 | let subject = RegistrationPostalZipViewController.instantiateFromStoryboard(fulfillmentStoryboard)
11 | subject.bidDetails = testBidDetails()
12 | expect(subject).to( haveValidSnapshot() )
13 | }
14 |
15 | it("looks right with existing postal code") {
16 | let subject = RegistrationPostalZipViewController.instantiateFromStoryboard(fulfillmentStoryboard)
17 | subject.bidDetails = testBidDetails()
18 | subject.bidDetails.newUser.zipCode.value = "A1A1A1"
19 | expect(subject).to( haveValidSnapshot() )
20 | }
21 |
22 | it("unbinds bidDetails on viewWillDisappear:") {
23 | let runLifecycleOfViewController = { (bidDetails: BidDetails) -> RegistrationPostalZipViewController in
24 | let subject = RegistrationPostalZipViewController.instantiateFromStoryboard(fulfillmentStoryboard)
25 | subject.bidDetails = bidDetails
26 | subject.loadViewProgrammatically()
27 | subject.viewWillDisappear(false)
28 | return subject
29 | }
30 |
31 | let bidDetails = testBidDetails()
32 | _ = runLifecycleOfViewController(bidDetails)
33 |
34 | expect { runLifecycleOfViewController(bidDetails) }.toNot( raiseException() )
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/KioskTests/Bid Fulfillment/StripeManagerTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | import RxSwift
4 | @testable
5 | import Kiosk
6 | import Stripe
7 |
8 | class StripeManagerTests: QuickSpec {
9 | override func spec() {
10 | var subject: StripeManager!
11 | var testStripeClient: TestSTPAPIClient!
12 | var disposeBag: DisposeBag!
13 |
14 | beforeEach {
15 | Stripe.setDefaultPublishableKey("some key")
16 |
17 | subject = StripeManager()
18 | testStripeClient = TestSTPAPIClient()
19 | subject.stripeClient = testStripeClient
20 | disposeBag = DisposeBag()
21 | }
22 |
23 | afterEach {
24 | Stripe.setDefaultPublishableKey("")
25 | }
26 |
27 | it("sends the correct token upon success") {
28 | waitUntil { done in
29 | subject.registerCard(digits: "", month: 0, year: 0, securityCode: "", postalCode: "").subscribe(onNext: { (object) in
30 | let token = object
31 |
32 | expect(token.tokenId) == "12345"
33 | done()
34 | }).disposed(by: disposeBag)
35 | }
36 | }
37 |
38 | it("sends the correct token upon success") {
39 | var completed = false
40 | waitUntil { done in
41 | subject.registerCard(digits: "", month: 0, year: 0, securityCode: "", postalCode: "").subscribe(onCompleted: {
42 | completed = true
43 | done()
44 | }).disposed(by: disposeBag)
45 | }
46 |
47 | expect(completed) == true
48 | }
49 |
50 | it("sends error upon success") {
51 | testStripeClient.succeed = false
52 |
53 | var errored = false
54 | waitUntil { done in
55 | subject.registerCard(digits: "", month: 0, year: 0, securityCode: "", postalCode: "").subscribe(onError: { _ in
56 | errored = true
57 | done()
58 | }).disposed(by: disposeBag)
59 | }
60 |
61 | expect(errored) == true
62 | }
63 | }
64 | }
65 |
66 | class TestSTPAPIClient: Clientable {
67 | var succeed = true
68 | var token = TestToken()
69 |
70 | func kiosk_createToken(withCard card: STPCardParams, completion: ((Tokenable?, Error?) -> Void)?) {
71 | if succeed {
72 | completion?(token, nil)
73 | } else {
74 | completion?(nil, TestError.Default)
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/KioskTests/Bid Fulfillment/YourBiddingDetailsViewControllerTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | @testable
4 | import Kiosk
5 | import RxSwift
6 | import Nimble_Snapshots
7 |
8 | class YourBiddingDetailsViewControllerTests: QuickSpec {
9 | override func spec() {
10 | it("displays bidder number and PIN") {
11 | let subject = YourBiddingDetailsViewController.instantiateFromStoryboard(fulfillmentStoryboard)
12 | subject.bidDetails = testBidDetails()
13 | subject.bidDetails.paddleNumber.value = "14589"
14 | subject.bidDetails.bidderPIN.value = "4468"
15 |
16 | expect(subject).to( haveValidSnapshot() )
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/KioskTests/HelpViewControllerTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | import Nimble_Snapshots
4 | import RxSwift
5 | @testable
6 | import Kiosk
7 |
8 | class HelpViewControllerConfiguration: QuickConfiguration {
9 | override class func configure(_ configuration: Configuration) {
10 | sharedExamples("a help view controller") { (sharedExampleContext: @escaping SharedExampleContext) in
11 | var subject: HelpViewController!
12 |
13 | beforeEach {
14 | subject = sharedExampleContext()["subject"] as! HelpViewController!
15 | }
16 |
17 | it("looks correct") {
18 | expect(subject) == snapshot()
19 | return
20 | }
21 | }
22 | }
23 | }
24 |
25 |
26 | class HelpViewControllerTests: QuickSpec {
27 | override func spec() {
28 | var subject: HelpViewController!
29 |
30 | beforeEach {
31 | subject = HelpViewController()
32 | // Default to no buyers premium
33 | subject.hasBuyersPremium = Observable.just(false).take(1)
34 | }
35 |
36 | itBehavesLike("a help view controller") { ["subject": subject] }
37 |
38 | describe("with a buyers premium") {
39 | beforeEach {
40 | subject.hasBuyersPremium = Observable.just(true).take(1)
41 | }
42 |
43 | itBehavesLike("a help view controller") { ["subject": subject] }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/KioskTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 5.9.3
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 2018.02.07
23 |
24 |
25 |
--------------------------------------------------------------------------------
/KioskTests/KioskTests.swift:
--------------------------------------------------------------------------------
1 | import ObjectiveC
2 | import UIKit
3 | @testable
4 | import Kiosk
5 |
6 | var AssociatedObjectHandle: UInt8 = 0
7 |
8 | extension UIViewController {
9 | func wrapInFulfillmentNav() -> UIViewController {
10 | let nav = FulfillmentNavigationController(rootViewController: self)
11 | nav.auctionID = ""
12 | objc_setAssociatedObject(self, &AssociatedObjectHandle, nav, .OBJC_ASSOCIATION_RETAIN)
13 | return self
14 | }
15 | }
16 |
17 | let auctionStoryboard = UIStoryboard.auction()
18 | let fulfillmentStoryboard = UIStoryboard.fulfillment()
19 |
--------------------------------------------------------------------------------
/KioskTests/Models/ArtistTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | @testable
4 | import Kiosk
5 |
6 | class ArtistTests: QuickSpec {
7 | override func spec() {
8 |
9 | it("converts from JSON") {
10 |
11 | let id = "artist-1"
12 | let name = "Artist 1"
13 | let data = ["id" : id, "name": name]
14 |
15 | let artist = Artist.fromJSON(data)
16 |
17 | expect(artist.id) == id
18 | expect(artist.name) == name
19 | }
20 |
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/KioskTests/Models/BidTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | @testable
4 | import Kiosk
5 |
6 | class BidTests: QuickSpec {
7 | override func spec() {
8 | it("converts from JSON") {
9 | let id = "saf32sadasd"
10 | let amount: Currency = 100000
11 | let data:[String: Any] = ["id":id as AnyObject , "amount_cents" : amount ]
12 |
13 | let bid = Bid.fromJSON(data)
14 |
15 | expect(bid.id) == id
16 | expect(bid.amountCents) == amount
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/KioskTests/Models/BidderPositionTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | @testable
4 | import Kiosk
5 |
6 | class BidderPositionTests: QuickSpec {
7 | override func spec() {
8 |
9 | it("converts from JSON") {
10 | let id = "saf32sadasd"
11 | let maxBidAmountCents: Currency = 123123123
12 |
13 | let bidID = "saf32sadasd"
14 | let bidAmount: Currency = 100000
15 | let bidData:[String: Any] = ["id": bidID as AnyObject, "amount_cents" : bidAmount ]
16 |
17 | let data:[String: Any] = ["id":id as AnyObject , "max_bid_amount_cents" : maxBidAmountCents, "highest_bid":bidData]
18 |
19 | let position = BidderPosition.fromJSON(data)
20 |
21 | expect(position.id) == id
22 | expect(position.maxBidAmountCents) == maxBidAmountCents
23 | expect(position.highestBid!.id) == bidID
24 | expect(position.highestBid!.amountCents) == bidAmount
25 | }
26 |
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/KioskTests/Models/BidderTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | @testable
4 | import Kiosk
5 |
6 | class BidderTests: QuickSpec {
7 | override func spec() {
8 |
9 | it("converts from JSON") {
10 | let id = "324ddf445"
11 | let saleID = "asdkhaskda"
12 | let data:[String: Any] = ["id":id , "sale" : ["id": saleID]]
13 |
14 | let bidder = Bidder.fromJSON(data)
15 |
16 | expect(bidder.id) == id
17 | expect(bidder.saleID) == saleID
18 | }
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/KioskTests/Models/ImageTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | @testable
4 | import Kiosk
5 |
6 | let size = CGSize(width: 100, height: 100)
7 |
8 | class ImageTests: QuickSpec {
9 | override func spec() {
10 | let id = "wah-wah"
11 | let url = "http://url.com"
12 |
13 | it("converts from JSON") {
14 |
15 | let imageFormats = ["big", "small", "patch"]
16 | let data:[String: Any] = [ "id": id as AnyObject, "image_url": url, "image_versions": imageFormats, "original_width": size.width, "original_height": size.height]
17 |
18 | let image = Image.fromJSON(data)
19 |
20 | expect(image.id) == id
21 | expect(image.imageFormatString) == url
22 | expect(image.imageVersions.count) == imageFormats.count
23 | expect(image.imageSize) == size
24 | }
25 |
26 | it("generates a thumbnail url") {
27 | var image = self.image(forVersion: "large")
28 | expect(image.thumbnailURL()).toNot( beNil() )
29 |
30 | image = self.image(forVersion: "medium")
31 | expect(image.thumbnailURL()).toNot( beNil() )
32 |
33 | image = self.image(forVersion: "larger")
34 | expect(image.thumbnailURL()).toNot( beNil() )
35 | }
36 |
37 | it("handles unknown image formats"){
38 | let image = self.image(forVersion: "unknown")
39 | expect(image.thumbnailURL()).to(beNil())
40 | }
41 |
42 | it("handles incorrect image_versions JSON") {
43 | let data:[String: Any] = [ "id": id, "image_url": url, "image_versions": "something invalid"]
44 |
45 | expect(Image.fromJSON(data)).toNot( throwError() )
46 | }
47 |
48 | it("assumes it's not default if not specified") {
49 | let image = Image.fromJSON([
50 | "id": "",
51 | "image_url":"http://image.com/:version.jpg",
52 | "image_versions" : ["small"],
53 | "original_width": size.width,
54 | "original_height": size.height
55 | ])
56 |
57 | expect(image.isDefault) == false
58 | }
59 | }
60 |
61 | func image(forVersion version:String) -> Image {
62 | return Image.fromJSON([
63 | "id": "",
64 | "image_url":"http://image.com/:version.jpg",
65 | "image_versions" : [version],
66 | "original_width": size.width,
67 | "original_height": size.height
68 | ])
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/KioskTests/Models/SaleTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | @testable
4 | import Kiosk
5 |
6 | class SaleTests: QuickSpec {
7 | func stringFromDate(_ date: Date) -> String {
8 | return ISO8601DateFormatter().string(from: date)
9 | }
10 |
11 | override func spec() {
12 |
13 | it("converts from JSON") {
14 | let id = "saf32sadasd"
15 | let isAuction = true
16 | let startDate = "2014-09-21T19:22:24Z"
17 | let endDate = "2015-09-24T19:22:24Z"
18 | let name = "name"
19 | let data:[String: Any] = ["id":id , "is_auction" : isAuction, "name": name, "start_at":startDate, "end_at":endDate]
20 |
21 | let sale = Sale.fromJSON(data)
22 |
23 | expect(sale.id) == id
24 | expect(sale.isAuction) == isAuction
25 | expect(yearFromDate(sale.startDate)) == 2014
26 | expect(yearFromDate(sale.endDate!)) == 2015
27 | expect(sale.name) == name
28 | }
29 |
30 | describe("active state") {
31 | it("is inactive for past auction") {
32 | let artsyTime = SystemTime()
33 | artsyTime.systemTimeInterval = 0
34 |
35 | let date = NSDate.distantPast
36 | let dateString = self.stringFromDate(date)
37 |
38 | let data:[String: AnyObject] = ["start_at": dateString as AnyObject, "end_at" : dateString as AnyObject]
39 |
40 | let sale = Sale.fromJSON(data)
41 | expect(sale.isActive(artsyTime)) == false
42 | }
43 |
44 | it("is active for current auction") {
45 | let artsyTime = SystemTime()
46 | artsyTime.systemTimeInterval = 0
47 |
48 | let pastDate = NSDate.distantPast
49 | let pastString = self.stringFromDate(pastDate)
50 |
51 | let futureDate = NSDate.distantFuture
52 | let futureString = self.stringFromDate(futureDate)
53 |
54 | let data:[String: AnyObject] = ["start_at": pastString as AnyObject, "end_at" : futureString as AnyObject]
55 |
56 | let sale = Sale.fromJSON(data)
57 |
58 | expect(sale.isActive(artsyTime)) == true
59 | }
60 |
61 | it("is inactive for future auction") {
62 | let artsyTime = SystemTime()
63 | artsyTime.systemTimeInterval = 0
64 |
65 | let date = NSDate.distantFuture
66 | let dateString = self.stringFromDate(date)
67 |
68 | let data:[String: AnyObject] = ["start_at": dateString as AnyObject, "end_at" : dateString as AnyObject]
69 |
70 | let sale = Sale.fromJSON(data)
71 | expect(sale.isActive(artsyTime)) == false
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/KioskTests/Models/SystemTimeTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | @testable
4 | import Kiosk
5 | import RxSwift
6 |
7 | // Note, stubbed json contains a date in 2422
8 | // If this is an issue ( hello future people! ) then move it along a few centuries.
9 |
10 | class SystemTimeTests: QuickSpec {
11 | override func spec() {
12 |
13 | var disposeBag: DisposeBag!
14 | var networking: Networking!
15 |
16 | beforeEach {
17 | networking = Networking.newStubbingNetworking()
18 | disposeBag = DisposeBag()
19 | }
20 |
21 | describe("in sync") {
22 |
23 | it("returns true") {
24 | let time = SystemTime()
25 | time
26 | .sync(networking)
27 | .subscribe(onNext: { (_) in
28 | expect(time.inSync()) == true
29 | return
30 | })
31 | .disposed(by: disposeBag)
32 | }
33 |
34 | it("returns a date in the future") {
35 | let time = SystemTime()
36 | time
37 | .sync(networking)
38 | .subscribe(onNext: { (_) in
39 | let currentYear = yearFromDate(Date())
40 | let timeYear = yearFromDate(time.date())
41 |
42 | expect(timeYear) > currentYear
43 | expect(timeYear) == 2422
44 |
45 | })
46 | .disposed(by: disposeBag)
47 | }
48 | }
49 |
50 | describe("not in sync") {
51 | it("returns false") {
52 | let time = SystemTime()
53 | expect(time.inSync()) == false
54 | }
55 |
56 | it("returns current time") {
57 | let time = SystemTime()
58 | let currentYear = yearFromDate(Date())
59 | let timeYear = yearFromDate(time.date())
60 |
61 | expect(timeYear) == currentYear
62 | }
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/AppViewControllerTests/looks_right_offline@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/AppViewControllerTests/looks_right_offline@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ConfirmYourBidArtsyLoginViewControllerTests/looks_right_by_default@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ConfirmYourBidArtsyLoginViewControllerTests/looks_right_by_default@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ConfirmYourBidEnterYourEmailViewControllerTests/looks_right_by_default@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ConfirmYourBidEnterYourEmailViewControllerTests/looks_right_by_default@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ConfirmYourBidPINViewControllerTests/looks_right_by_default@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ConfirmYourBidPINViewControllerTests/looks_right_by_default@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/HelpViewControllerTests/a_help_view_controller__looks_correct@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/HelpViewControllerTests/a_help_view_controller__looks_correct@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/HelpViewControllerTests/with_a_buyers_premium__a_help_view_controller__looks_correct@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/HelpViewControllerTests/with_a_buyers_premium__a_help_view_controller__looks_correct@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_artworks_not_for_sale__a_listings_controller__alphabetical@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_artworks_not_for_sale__a_listings_controller__alphabetical@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_artworks_not_for_sale__a_listings_controller__grid@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_artworks_not_for_sale__a_listings_controller__grid@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_artworks_not_for_sale__a_listings_controller__highest_bid@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_artworks_not_for_sale__a_listings_controller__highest_bid@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_artworks_not_for_sale__a_listings_controller__least_bids@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_artworks_not_for_sale__a_listings_controller__least_bids@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_artworks_not_for_sale__a_listings_controller__lowest_bid@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_artworks_not_for_sale__a_listings_controller__lowest_bid@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_artworks_not_for_sale__a_listings_controller__most_bids@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_artworks_not_for_sale__a_listings_controller__most_bids@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_lot_numbers__a_listings_controller__alphabetical@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_lot_numbers__a_listings_controller__alphabetical@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_lot_numbers__a_listings_controller__grid@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_lot_numbers__a_listings_controller__grid@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_lot_numbers__a_listings_controller__highest_bid@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_lot_numbers__a_listings_controller__highest_bid@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_lot_numbers__a_listings_controller__least_bids@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_lot_numbers__a_listings_controller__least_bids@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_lot_numbers__a_listings_controller__lowest_bid@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_lot_numbers__a_listings_controller__lowest_bid@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_lot_numbers__a_listings_controller__most_bids@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__with_lot_numbers__a_listings_controller__most_bids@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__without_lot_numbers__a_listings_controller__alphabetical@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__without_lot_numbers__a_listings_controller__alphabetical@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__without_lot_numbers__a_listings_controller__grid@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__without_lot_numbers__a_listings_controller__grid@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__without_lot_numbers__a_listings_controller__highest_bid@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__without_lot_numbers__a_listings_controller__highest_bid@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__without_lot_numbers__a_listings_controller__least_bids@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__without_lot_numbers__a_listings_controller__least_bids@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__without_lot_numbers__a_listings_controller__lowest_bid@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__without_lot_numbers__a_listings_controller__lowest_bid@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__without_lot_numbers__a_listings_controller__most_bids@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ListingsViewControllerTests/when_displaying_stubbed_contents__without_lot_numbers__a_listings_controller__most_bids@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/LoadingViewControllerTests/default__placing_a_bid@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/LoadingViewControllerTests/default__placing_a_bid@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/LoadingViewControllerTests/default__registering_a_user@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/LoadingViewControllerTests/default__registering_a_user@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/LoadingViewControllerTests/ending__placing_bid_error_due_to_outbid@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/LoadingViewControllerTests/ending__placing_bid_error_due_to_outbid@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/LoadingViewControllerTests/ending__placing_bid_succeeded_but_not_resolved@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/LoadingViewControllerTests/ending__placing_bid_succeeded_but_not_resolved@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/LoadingViewControllerTests/ending__placing_bid_success_highest@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/LoadingViewControllerTests/ending__placing_bid_success_highest@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/LoadingViewControllerTests/ending__placing_bid_success_not_highest@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/LoadingViewControllerTests/ending__placing_bid_success_not_highest@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/LoadingViewControllerTests/ending__registering_user_not_resolved@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/LoadingViewControllerTests/ending__registering_user_not_resolved@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/LoadingViewControllerTests/ending__registering_user_success@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/LoadingViewControllerTests/ending__registering_user_success@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/LoadingViewControllerTests/errors__correctly_placing_a_bid@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/LoadingViewControllerTests/errors__correctly_placing_a_bid@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/LoadingViewControllerTests/errors__correctly_registering_a_user@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/LoadingViewControllerTests/errors__correctly_registering_a_user@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ManualCreditCardInputViewControllerTests/after_CC_is_entered_with_valid_dates__uses_registerButtonCommand_enabledness_for_date_button@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ManualCreditCardInputViewControllerTests/after_CC_is_entered_with_valid_dates__uses_registerButtonCommand_enabledness_for_date_button@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ManualCreditCardInputViewControllerTests/asks_for_CC_number_by_default@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ManualCreditCardInputViewControllerTests/asks_for_CC_number_by_default@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ManualCreditCardInputViewControllerTests/enables_CC_entry_field_when_CC_is_valid@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ManualCreditCardInputViewControllerTests/enables_CC_entry_field_when_CC_is_valid@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/ManualCreditCardInputViewControllerTests/shows_errors@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/ManualCreditCardInputViewControllerTests/shows_errors@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/PlaceBidViewControllerTests/looks_right_by_default@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/PlaceBidViewControllerTests/looks_right_by_default@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/PlaceBidViewControllerTests/looks_right_with_a_custom_saleArtwork@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/PlaceBidViewControllerTests/looks_right_with_a_custom_saleArtwork@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/PlaceBidViewControllerTests/with_bids__a_bid_view_controller_view_controller__looks_correct@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/PlaceBidViewControllerTests/with_bids__a_bid_view_controller_view_controller__looks_correct@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/PlaceBidViewControllerTests/with_bids__a_bid_view_controller_view_controller__with_lot_number__looks_correct@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/PlaceBidViewControllerTests/with_bids__a_bid_view_controller_view_controller__with_lot_number__looks_correct@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/PlaceBidViewControllerTests/with_bids__a_bid_view_controller_view_controller__without_lot_number__looks_correct@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/PlaceBidViewControllerTests/with_bids__a_bid_view_controller_view_controller__without_lot_number__looks_correct@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/PlaceBidViewControllerTests/with_no_bids__a_bid_view_controller_view_controller__looks_correct@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/PlaceBidViewControllerTests/with_no_bids__a_bid_view_controller_view_controller__looks_correct@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/PlaceBidViewControllerTests/with_no_bids__a_bid_view_controller_view_controller__with_lot_number__looks_correct@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/PlaceBidViewControllerTests/with_no_bids__a_bid_view_controller_view_controller__with_lot_number__looks_correct@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/PlaceBidViewControllerTests/with_no_bids__a_bid_view_controller_view_controller__without_lot_number__looks_correct@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/PlaceBidViewControllerTests/with_no_bids__a_bid_view_controller_view_controller__without_lot_number__looks_correct@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/PlaceBidViewControllerTests/with_no_bids__with_a_buyers_premium__a_bid_view_controller_view_controller__with_lot_number__looks_correct@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/PlaceBidViewControllerTests/with_no_bids__with_a_buyers_premium__a_bid_view_controller_view_controller__with_lot_number__looks_correct@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/PlaceBidViewControllerTests/with_no_bids__with_a_buyers_premium__a_bid_view_controller_view_controller__without_lot_number__looks_correct@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/PlaceBidViewControllerTests/with_no_bids__with_a_buyers_premium__a_bid_view_controller_view_controller__without_lot_number__looks_correct@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/RegisterFlowViewTests/not_requiring_zip_code__a_register_flow_view__handles_full_data@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/RegisterFlowViewTests/not_requiring_zip_code__a_register_flow_view__handles_full_data@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/RegisterFlowViewTests/not_requiring_zip_code__a_register_flow_view__handles_highlighted_index@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/RegisterFlowViewTests/not_requiring_zip_code__a_register_flow_view__handles_highlighted_index@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/RegisterFlowViewTests/not_requiring_zip_code__a_register_flow_view__handles_partial_data@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/RegisterFlowViewTests/not_requiring_zip_code__a_register_flow_view__handles_partial_data@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/RegisterFlowViewTests/not_requiring_zip_code__a_register_flow_view__looks_right_by_default@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/RegisterFlowViewTests/not_requiring_zip_code__a_register_flow_view__looks_right_by_default@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/RegisterFlowViewTests/requiring_zip_code__a_register_flow_view__handles_full_data@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/RegisterFlowViewTests/requiring_zip_code__a_register_flow_view__handles_full_data@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/RegisterFlowViewTests/requiring_zip_code__a_register_flow_view__handles_highlighted_index@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/RegisterFlowViewTests/requiring_zip_code__a_register_flow_view__handles_highlighted_index@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/RegisterFlowViewTests/requiring_zip_code__a_register_flow_view__handles_partial_data@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/RegisterFlowViewTests/requiring_zip_code__a_register_flow_view__handles_partial_data@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/RegisterFlowViewTests/requiring_zip_code__a_register_flow_view__looks_right_by_default@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/RegisterFlowViewTests/requiring_zip_code__a_register_flow_view__looks_right_by_default@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/RegistrationEmailViewControllerTests/looks_right_by_default@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/RegistrationEmailViewControllerTests/looks_right_by_default@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/RegistrationEmailViewControllerTests/looks_right_with_existing_email@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/RegistrationEmailViewControllerTests/looks_right_with_existing_email@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/RegistrationMobileViewControllerTests/looks_right_by_default@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/RegistrationMobileViewControllerTests/looks_right_by_default@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/RegistrationMobileViewControllerTests/looks_right_with_existing_mobile@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/RegistrationMobileViewControllerTests/looks_right_with_existing_mobile@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/RegistrationPasswordViewControllerTests/looks_right_by_default@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/RegistrationPasswordViewControllerTests/looks_right_by_default@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/RegistrationPasswordViewControllerTests/looks_right_with_a_valid_password@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/RegistrationPasswordViewControllerTests/looks_right_with_a_valid_password@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/RegistrationPasswordViewControllerTests/looks_right_with_an_existing_email@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/RegistrationPasswordViewControllerTests/looks_right_with_an_existing_email@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/RegistrationPasswordViewControllerTests/looks_right_with_an_invalid_password@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/RegistrationPasswordViewControllerTests/looks_right_with_an_invalid_password@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/RegistrationPostalZipViewControllerTests/looks_right_by_default@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/RegistrationPostalZipViewControllerTests/looks_right_by_default@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/RegistrationPostalZipViewControllerTests/looks_right_with_existing_postal_code@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/RegistrationPostalZipViewControllerTests/looks_right_with_existing_postal_code@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/SaleArtworkDetailsViewControllerTests/looks_ok_by_default@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/SaleArtworkDetailsViewControllerTests/looks_ok_by_default@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/SaleArtworkDetailsViewControllerTests/with_a_buyers_premium__a_sale_artwork_details_view_controller__looks_ok_by_default@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/SaleArtworkDetailsViewControllerTests/with_a_buyers_premium__a_sale_artwork_details_view_controller__looks_ok_by_default@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/SaleArtworkDetailsViewControllerTests/with_an_artwork_not_for_sale__a_sale_artwork_details_view_controller__looks_ok_by_default@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/SaleArtworkDetailsViewControllerTests/with_an_artwork_not_for_sale__a_sale_artwork_details_view_controller__looks_ok_by_default@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/SaleArtworkDetailsViewControllerTests/with_lot_numbers__a_sale_artwork_details_view_controller__looks_ok_by_default@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/SaleArtworkDetailsViewControllerTests/with_lot_numbers__a_sale_artwork_details_view_controller__looks_ok_by_default@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/SaleArtworkDetailsViewControllerTests/without_lot_numbers__a_sale_artwork_details_view_controller__looks_ok_by_default@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/SaleArtworkDetailsViewControllerTests/without_lot_numbers__a_sale_artwork_details_view_controller__looks_ok_by_default@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/SwitchViewSpec/default@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/SwitchViewSpec/default@2x.png
--------------------------------------------------------------------------------
/KioskTests/ReferenceImages/YourBiddingDetailsViewControllerTests/displays_bidder_number_and_PIN@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/ReferenceImages/YourBiddingDetailsViewControllerTests/displays_bidder_number_and_PIN@2x.png
--------------------------------------------------------------------------------
/KioskTests/SaleArtworkDetailsViewControllerTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | @testable
4 | import Kiosk
5 | import Nimble_Snapshots
6 | import SDWebImage
7 |
8 | class SaleArtworkDetailsViewControllerConfiguration: QuickConfiguration {
9 | override class func configure(_ configuration: Configuration) {
10 | sharedExamples("a sale artwork details view controller") { (sharedExampleContext: @escaping SharedExampleContext) in
11 | var subject: SaleArtworkDetailsViewController!
12 |
13 | beforeEach{
14 | subject = sharedExampleContext()["subject"] as! SaleArtworkDetailsViewController!
15 | }
16 |
17 | it("looks ok by default") {
18 | expect(subject) == snapshot()
19 | }
20 | }
21 | }
22 | }
23 |
24 | class SaleArtworkDetailsViewControllerTests: QuickSpec {
25 | let imageCache = SDImageCache.shared()
26 | override func spec() {
27 | var subject: SaleArtworkDetailsViewController!
28 |
29 | beforeEach {
30 | subject = testSaleArtworkViewController()
31 | subject.allowAnimations = false
32 |
33 | let image = UIImage.testImage(named: "artwork", ofType: "jpg")
34 | self.imageCache?.store(image, forKey: "http://example.com/large.jpg")
35 | }
36 |
37 | describe("without lot numbers") {
38 | itBehavesLike("a sale artwork details view controller") { ["subject": subject] }
39 | }
40 |
41 | describe("with lot numbers") {
42 | beforeEach {
43 | subject.saleArtwork.lotLabel = "13"
44 | }
45 |
46 | itBehavesLike("a sale artwork details view controller") { ["subject": subject] }
47 | }
48 |
49 | describe("with a buyers premium") {
50 | beforeEach {
51 | subject.buyersPremium = { BuyersPremium(id: "id", name: "name") }
52 | }
53 |
54 | itBehavesLike("a sale artwork details view controller") { ["subject": subject] }
55 | }
56 |
57 | describe("with an artwork not for sale") {
58 | beforeEach {
59 | subject.saleArtwork.artwork.soldStatus = true
60 | }
61 |
62 | itBehavesLike("a sale artwork details view controller") { ["subject": subject] }
63 | }
64 | }
65 | }
66 |
67 | func testSaleArtworkViewController(storyboard: UIStoryboard = auctionStoryboard, saleArtwork: SaleArtwork = testSaleArtwork()) -> SaleArtworkDetailsViewController {
68 | let subject = SaleArtworkDetailsViewController.instantiateFromStoryboard(storyboard)
69 | subject.saleArtwork = saleArtwork
70 | subject.buyersPremium = { nil }
71 | subject.provider = Networking.newStubbingNetworking()
72 |
73 | return subject
74 | }
75 |
--------------------------------------------------------------------------------
/KioskTests/SwitchViewSpec.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | @testable
4 | import Kiosk
5 | import Nimble_Snapshots
6 |
7 | class SwitchViewSpec: QuickSpec {
8 | override func spec() {
9 | it("looks correct configured with two buttons") {
10 | let titles = ["First title", "Second Title"]
11 | let switchView = SwitchView(buttonTitles: titles)
12 | switchView.frame = CGRect(origin: CGPoint.zero, size: CGSize(width: 400, height: switchView.intrinsicContentSize.height))
13 |
14 | expect(switchView).to(haveValidSnapshot(named:"default"))
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/KioskTests/UILabelExtensionsTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | @testable
4 | import Kiosk
5 |
6 | class UILabelExtensionsTests: QuickSpec {
7 | override func spec() {
8 | it("makes labels non-opaque") {
9 | let subject = UILabel()
10 | subject.isOpaque = true
11 | subject.makeTransparent()
12 |
13 | expect(subject.isOpaque) == false
14 | }
15 |
16 | it("makes labels with clear backgrounds") {
17 | let subject = UILabel()
18 | subject.backgroundColor = .red
19 | subject.makeTransparent()
20 |
21 | expect(subject.backgroundColor) == .clear
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/KioskTests/XAppTokenSpec.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | @testable
4 | import Kiosk
5 |
6 | class XAppTokenSpec: QuickSpec {
7 | override func spec() {
8 | var defaults: UserDefaults!
9 | var token: XAppToken!
10 |
11 | beforeEach {
12 | defaults = UserDefaults()
13 | token = XAppToken(defaults: defaults)
14 | }
15 |
16 | it("returns correct data") {
17 | let key = "some key"
18 | let expiry = Date(timeIntervalSinceNow: 1000)
19 | setDefaultsKeys(defaults, key: key, expiry: expiry)
20 |
21 | expect(token.token).to( equal(key) )
22 | expect(token.expiry).to( beCloseTo(expiry, within: 1) )
23 | }
24 |
25 | it("correctly calculates validity for expired tokens") {
26 | let key = "some key"
27 | let past = Date(timeIntervalSinceNow: -1000)
28 | setDefaultsKeys(defaults, key: key, expiry: past)
29 |
30 | expect(token.isValid).to( beFalsy() )
31 | }
32 |
33 | it("correctly calculates validity for non-expired tokens") {
34 | let key = "some key"
35 | let future = Date(timeIntervalSinceNow: 1000)
36 | setDefaultsKeys(defaults, key: key, expiry: future)
37 |
38 | expect(token.isValid).to( beTruthy() )
39 | }
40 |
41 | it("correctly calculates validity for empty keys") {
42 | let key = ""
43 | let future = Date(timeIntervalSinceNow: 1000)
44 | setDefaultsKeys(defaults, key: key, expiry: future)
45 |
46 | expect(token.isValid).to( beFalsy() )
47 | }
48 |
49 | it("properly calculates validity for missing tokens") {
50 | setDefaultsKeys(defaults, key: nil, expiry: nil)
51 |
52 | expect(token.isValid).to( beFalsy() )
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/KioskTests/artwork.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/KioskTests/artwork.jpg
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2014-2018 Art.sy, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9 | of the Software, and to permit persons to whom the Software is furnished to do
10 | 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 |
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | source 'https://github.com/artsy/Specs.git'
2 | source 'https://cdn.cocoapods.org/'
3 |
4 | plugin 'cocoapods-keys', {
5 | :project => 'Eidolon',
6 | :keys => [
7 | 'ArtsyAPIClientSecret',
8 | 'ArtsyAPIClientKey',
9 | 'HockeyProductionSecret',
10 | 'HockeyBetaSecret',
11 | 'SegmentWriteKey',
12 | 'CardflightProductionAPIClientKey',
13 | 'CardflightProductionMerchantAccountToken',
14 | 'StripeProductionPublishableKey',
15 | 'CardflightStagingAPIClientKey',
16 | 'CardflightStagingMerchantAccountToken',
17 | 'StripeStagingPublishableKey'
18 | ]
19 | }
20 |
21 | platform :ios, '10.0'
22 | use_frameworks!
23 |
24 | # Yep.
25 | inhibit_all_warnings!
26 |
27 | target 'Kiosk' do
28 |
29 | # Artsy stuff
30 | pod 'Artsy+UIColors'
31 | pod 'Artsy+UILabels'
32 | pod 'Artsy-UIButtons'
33 |
34 | if ENV['ARTSY_STAFF_MEMBER'] != nil || ENV['CI'] != nil
35 | pod 'Artsy+UIFonts'
36 | else
37 | pod 'Artsy+OSSUIFonts'
38 | end
39 |
40 | pod 'ORStackView', '2.0'
41 | pod 'FLKAutoLayout', '0.1.1'
42 | pod 'ARCollectionViewMasonryLayout', '~> 2.0.0'
43 | pod 'SDWebImage', '~> 3.7'
44 | pod 'SVProgressHUD'
45 |
46 | # Required as a workaround for https://github.com/bitstadium/HockeySDK-iOS/pull/421
47 | pod 'HockeySDK-Source', git: 'https://github.com/bitstadium/HockeySDK-iOS.git'
48 | pod 'ARAnalytics/Segmentio'
49 | pod 'ARAnalytics/HockeyApp'
50 |
51 | pod 'CardFlight-v4'
52 | pod 'Stripe', '14.0.1'
53 | pod 'ECPhoneNumberFormatter'
54 | pod 'UIImageViewAligned', :git => 'https://github.com/ashfurrow/UIImageViewAligned.git'
55 | pod 'DZNWebViewController', :git => 'https://github.com/orta/DZNWebViewController.git'
56 | pod 'ReachabilitySwift'
57 |
58 | pod 'UIView+BooleanAnimations'
59 | pod 'ARTiledImageView'
60 | pod 'XNGMarkdownParser'
61 | pod 'ISO8601DateFormatter'
62 |
63 | # Swift pods
64 | pod 'SwiftyJSON'
65 | pod 'RxSwift'
66 | pod 'RxCocoa'
67 | pod 'RxOptional'
68 | pod 'Moya/RxSwift'
69 | pod 'NSObject+Rx'
70 | pod 'Action'
71 |
72 | target 'KioskTests' do
73 | inherit! :search_paths
74 |
75 | pod 'FBSnapshotTestCase'
76 | pod 'Nimble-Snapshots'
77 | pod 'Quick'
78 | pod 'Nimble'
79 | pod 'RxNimble'
80 | pod 'Forgeries'
81 | pod 'RxBlocking'
82 |
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/docs/CI.md:
--------------------------------------------------------------------------------
1 | Continuous Integration
2 | ===========
3 |
4 | We are currently using Jenkins to run our CI with the aim to eventually switch to Travis when it adds Xcode 6 support. The vast majority of the CI work is done in our Makefile. Here's the commands being ran inside Jenkins itself:
5 |
6 | ``` shell
7 | #!/bin/bash
8 |
9 | source ~/.bash_profile
10 | rm -rf /Users/joe/Library/Developer/Xcode/DerivedData
11 | security unlock-keychain -p [username] /Users/joe/Library/Keychains/jenkins.keychain
12 |
13 | make prepare_ci
14 | make ci
15 |
16 | ```
17 |
18 | Under the hood we set the Xcode to the beta, run a build setup task, then a run task and switch back to the Xcode stable release. The reason for differentiating between building and testing in our steps is to reduce verbosity in reading the log in travis.
--------------------------------------------------------------------------------
/docs/PaymentProcessing.md:
--------------------------------------------------------------------------------
1 | Eidolon uses CardFlight for credit card tokenization. CardFlight is like ARAnalytics, but for payment processors. We do this for flexibility in terms of hardware and software.
2 |
3 | So. There are a few things you should know.
4 |
5 | CardFlight's concept of "Test" accounts doesn't really translate well for our purposes. So when testing on staging, _do not_ use the card reader. Instead, both the swipe and manual CC input screens have the following two buttons:
6 |
7 | 
8 |
9 | (You'll need to have Admin buttons visible to see them.)
10 |
11 | Hitting the thumbs up will use a testing credit card that succeeds, and thumbs down will use a testing credit card that will be declined.
12 |
13 | For testing against production, go to the Admin panel and use "Testing Credit Card Reader."
14 |
15 |
--------------------------------------------------------------------------------
/docs/artsy_dev.md:
--------------------------------------------------------------------------------
1 | Artsy Development
2 | =================
3 |
4 | Working for Artsy? Awesome. Setup instructions are a bit different from the README.
5 |
6 | You'll need some tools first. Xcode 9 is required, as well as the command-line tools (you probably already have these installed). After installing Xcode, run the following command:
7 |
8 | ```sh
9 | xcode-select --install
10 | ```
11 |
12 | You'll also need `bundler`, which you likely already have installed. If you don't, run:
13 |
14 | ```sh
15 | [sudo] gem install bundler
16 | ```
17 |
18 | Right, next clone the repo as usual and `cd` into it. You'll need to install the dependencies. The first command installs the tools required to use the second command, which installs the dependencies.
19 |
20 | ```sh
21 | bundle install
22 | bundle exec pod install
23 | ```
24 |
25 | When you run `pod install`, you'll be prompted for API keys. These are stored in the Engineering 1Password vault.
26 |
27 | Finally, you can run the tests to verify everything is set up right:
28 |
29 | ```sh
30 | bundle exec fastlane test
31 | ```
32 |
33 | It's possible you'll run into the following error:
34 |
35 | ```
36 | xcodebuild: error: Unable to find a destination matching the provided destination specifier:
37 | { OS:8.1, name:iPad Air }
38 | ```
39 |
40 | If that happens, it's because Xcode doesn't have the correct simulator installed. Open Xcode, then under the "Window" menu, select "Devices". Make sure to add an iPad Air with iOS 8.1. If you don't have iOS 8.1 installed, open Xcode preferences and install the iOS 8.1 simulator under the "Downloads" tab. After all that, re-run the tests to verify everything works.
41 |
--------------------------------------------------------------------------------
/docs/cc_testing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/docs/cc_testing.png
--------------------------------------------------------------------------------
/docs/deployment.md:
--------------------------------------------------------------------------------
1 | ### Deployment
2 |
3 | **Note**: These docs omit `bundle exec` in front of the commands.
4 |
5 | We deploy using [Fastlane](https://github.com/KrauseFx/fastlane). You'll need the following environment variables set up.
6 |
7 | ```
8 | export HOCKEY_API_TOKEN='THE_SECRET_TOKEN_YOU_GOT_FROM_1PASSWORD'
9 | export SLACK_URL='https://hooks.slack.com/services/REST_OF_THE_URL_FROM_1PASSWORD'
10 | ```
11 |
12 | They're in the Artsy Engineering 1Password vault. Just add them to your `.zshenv` (or equivalent file).
13 |
14 | Make sure Xcode Accounts has the it@ account in it.
15 |
16 | The changelog needs to be valid YAML, with an array of changelog entries to deploy.
17 |
18 | ```yaml
19 | upcoming:
20 | - some fix [done by some dev]
21 | ```
22 |
23 | Fastlane will take care of the rest. You can check out the specifics of what it does by executing `fastlane lanes`.
24 |
25 | To make a deploy, run the following:
26 |
27 | ```sh
28 | bundle exec fastlane deploy version:A.B.C
29 | ```
30 |
31 | The first time you deploy, you'll be asked to sign in to the developer portal through Fastlane. The password is in 1Password, too.
32 |
33 | Once the deploy is finished, a message with release notes will be posted to Slack.
34 |
35 | If you get an SSL error, like this:
36 |
37 | ```rb
38 | connect': SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed (Faraday::SSLError)
39 | ```
40 |
41 | Then it's most likely an RVM issue. Re-installing your ruby without binaries should fix the problem:
42 |
43 | ```sh
44 | rvm reinstall ruby-2.1.2 --disable-binary
45 | ```
46 |
--------------------------------------------------------------------------------
/docs/eidolon_preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artsy/eidolon/44486ed9149f16b3eb3a5e687f99ae078309f4fe/docs/eidolon_preview.jpg
--------------------------------------------------------------------------------
/docs/manual flows.md:
--------------------------------------------------------------------------------
1 | ## Potential Sign Up / Bid flows
2 |
3 | Register Button:
4 | * I have no account
5 |
6 | Bidding on an Artwork:
7 | * I have an account, but not set up to bid
8 | * I tried bidding but I haven't got an account
9 | * I have an account and I log in with my u:p
10 | * I have an account and I log in with my phone / passs
11 |
--------------------------------------------------------------------------------
/docs/overview.md:
--------------------------------------------------------------------------------
1 | ## App Structure
2 |
3 | The App is a Swift based RAC app. That means a reliance on functional programming. If any of this is new to you then it's recommended that you consult the books below. This means trying to wrap existing imperative patterns in terms of streams and signals.
4 |
5 | Unlike previous apps, this app aims to use Apples visual programming tools to reduce the actual code written as much as possible. If you are new to using Interface Builder and Storyboards I would recommend running though [this tutorial](http://www.raywenderlich.com/50308/storyboards-tutorial-in-ios-7-part-1) by Ray Wenderlich.
6 |
7 | We've tried to separate out chunks of functionality into separate Storyboards. For examples all of the bid view controllers are kept inside a single storyboard. When creating a new View Controller you need to give it a new Storyboard ID, and any required Segues. Then run `make storyboard_ids` to generate constants for usage in code.
8 |
9 | ## Recommended Reads
10 |
11 | In order of accessibility / good learning order.
12 |
13 | * The [Swift iBooks](https://itunes.apple.com/us/book/swift-programming-language/id881256329?mt=11)
14 | * Ash's book on [Functional Reactive Programming in iOS](https://leanpub.com/iosfrp)
15 | * objc.io's [Functional Programming in Swift](http://www.objc.io/books/)
16 |
17 | ## Testing
18 |
19 | We're using [Quick / Nimble](https://github.com/Quick/), as they seem to be in the lead for BDD testing on Swift. The test target can be ran with `⌘ + u` on the Kiosk Target. Due to Swift's rather awkward class privacy settings make sure the target membership for classes being tested is in both Kiosk & KioskTests.
20 |
--------------------------------------------------------------------------------
/fastlane/.env:
--------------------------------------------------------------------------------
1 | XCODE_SCHEME=Kiosk
2 | XCODE_WORKSPACE=Kiosk.xcworkspace
3 | XCODEPROJ=./Kiosk.xcodeproj
4 |
--------------------------------------------------------------------------------
/fastlane/Appfile:
--------------------------------------------------------------------------------
1 | app_identifier "net.artsy.kiosk.beta"
2 | apple_id "mobiledeploys@artsymail.com"
3 | team_name "ART SY INC"
4 |
--------------------------------------------------------------------------------
/fastlane/README.md:
--------------------------------------------------------------------------------
1 | fastlane documentation
2 | ================
3 | # Installation
4 |
5 | Make sure you have the latest version of the Xcode command line tools installed:
6 |
7 | ```
8 | xcode-select --install
9 | ```
10 |
11 | Install _fastlane_ using
12 | ```
13 | [sudo] gem install fastlane -NV
14 | ```
15 | or alternatively using `brew cask install fastlane`
16 |
17 | # Available Actions
18 | ### test
19 | ```
20 | fastlane test
21 | ```
22 | Run all iOS tests on an iPad
23 | ### oss_keys
24 | ```
25 | fastlane oss_keys
26 | ```
27 |
28 | ### oss
29 | ```
30 | fastlane oss
31 | ```
32 | Set all the API keys required for distribution
33 | ### deploy
34 | ```
35 | fastlane deploy
36 | ```
37 | Release a new beta version on Hockey
38 |
39 | This action does the following:
40 |
41 |
42 |
43 | - Verifies API keys are non-empty
44 |
45 | - Ensures a clean git status
46 |
47 | - Increment the build number
48 |
49 | - Build and sign the app
50 |
51 | - Upload the ipa file to hockey
52 |
53 | - Post a message to slack containing the download link
54 |
55 | - Commit and push the version bump
56 | ### storyboard_ids
57 | ```
58 | fastlane storyboard_ids
59 | ```
60 | Updates the storyboard identifier Swift values.
61 |
62 | ----
63 |
64 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run.
65 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
66 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
67 |
--------------------------------------------------------------------------------