├── .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 | ![Testing CC buttons](cc_testing.png) 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 | --------------------------------------------------------------------------------